Load/Mine: Hugging Face Model Repository Miner and Backdoor Hunt
Executive Summary
The attacker objective is free GPU compute, with durable remote access kept as insurance. Pluto Security reported the Load/Mine campaign on 2026-10-29: cryptominer and backdoor payloads delivered through 31 malicious Hugging Face model repositories across seven accounts, with roughly 32,376 downloads once linked models are counted (a download count, not a victim count). Wave 1 ran from 2026-09-21 to 2026-10-12 and was taken down. Wave 2 appeared within days and was still live at publication.
No malicious weights are involved. A victim loads a repository with trust_remote_code=True, the repository's config.json auto_map names a Python module such as modeling_evil2.EvilModel, and the module's top-level code runs during import, before the model class is returned. The flagship repository's model.safetensors is 42 bytes with no tensors. Hugging Face's ClamAV scan and the partner scanners returned clean because they match signatures and model formats, not what the Python does.
The payload plants a watchdog, hooks shell startup files, launches a UPX-packed miner from memory with memfd_create and fexecve under the name [kworker/u20:1], kills rival miners, and in three wave-2 builds opens a reconnecting reverse shell to 212.32.253.3. A later script targets container-to-host escape on GPUStack hosts. Targets are Linux GPU and inference hosts, notably Xinference and GPUStack, plus rented GPU machines, shared notebooks and CI jobs.
Highest-value defensive angle: the Falcon Linux sensor sees process creation, ELF writes, DNS and network connections on these hosts, which is enough to catch the miner, its masquerade, its drop directories and its infrastructure. It does not see the in-process writes to shell startup files, cron or authorized_keys, and it may not see the memfd launch cleanly. This pack leans on what the sensor sees and says plainly where it is blind.
Defender priority: run Q1 and Q7 estate-wide now, run Q3, Q5 and Q6 on every host that has loaded a model with custom code, and treat any hit as a compromised host: rotate the credentials the loading process could reach before cleaning, because killing the miner does not remove the planted access.
Source Review & Web Hunter Notes
| Tier | Source | Key Finding | Carry Forward |
|---|---|---|---|
| TIER 1 | Pluto Security: Load/Mine | Primary technical writeup and the full IOC section. Snapshot 01 holds the complete text including every IOC table. | yes |
| TIER 2 | BleepingComputer | Independent corroboration: 31 repositories, seven accounts, Xinference and GPUStack targeting, htt0118/qwen2-tiny still live. A direct fetch hit a Cloudflare challenge and was not bypassed; snapshot 02 is the fetch tool's summary, not verbatim page text. | partial |
| TIER 2 | HackerNoon | Sponsored write-up of the Pluto report using the 2026-10-25 figures (28 repositories, six accounts). Used only to cross-check network values already in Pluto's table. | partial |
| TIER 2 | FinanceWire press release | Pluto's own IOC field guide as a press release. Same origin as Tier 1, so it confirms transcription, not independence. | partial |
| TIER 3 | Artiverse | Secondary summary; confirms the persistence and reverse-shell description. | partial |
Decisions
- Counts differ by date. The primary writeup (as of 2026-10-28) gives 31 campaign repositories and 44 with linked models, 32,376 downloads. The earlier-figure syndications give 28 repositories and 21,614 downloads (as of 2026-10-25). The pack uses the primary figures.
- Linked families are excluded. Pluto labels the MONIADF-2 and antwoinhjro654 hl family, vaishupv, andrew3d, oifewuheu and KzuryHD repositories "linked" (shared technique, no proven shared ownership) and tracks the hl family separately. Their infrastructure, wallets, hashes and paths are not shipped.
- soumikmahato is shipped at medium confidence. It is one of the seven accounts in the 31-repository count but carries Indonesian-language comments and different tooling, and is not labelled proven. Its indicators ship as detect at medium.
- No prevent rows. Every indicator traces to Pluto alone; the other sources syndicate it. Two independent sources are required for prevent, so everything ships as detect.
- Unrelated story dropped. The BleepingComputer page also covers a fake Solana starter kit. It is a different incident and none of it is used here.
- Fetched-content check. The Pluto page includes "quick checks on a suspect host" shell commands. They were treated as data, not run, and the native hunts below are re-authored read-only checks. No fetched page contained text directing the analyst to run commands, fetch other URLs or skip steps.
- Omitted indicator. The soumikmahato
sslip.iohostname embeds an IPv4 address, which trips the provenance gate's right-boundary rule. It is left out of the importable CSV and noted here; it is a medium-confidence line of the pack.
Hunt Brief & Attack Chain
Hypotheses (highest fidelity first)
| # | Hypothesis | MITRE | Telemetry | Conf | Query |
|---|---|---|---|---|---|
| H1 | A Linux host talked to the operator's reverse-shell, payload, C2 or exfil addresses. | T1095, T1105, T1041 | Outbound network connection with owning process (CrowdStrike: NetworkConnectIP4. SentinelOne: IP Connect. Defender: DeviceNetworkEvents) | HIGH | Q1, Q10 |
| H2 | A known miner binary ran or was written (exact hash, binary name, pool login, wallet). | T1496, T1027.002 | Process creation with command line and hash; executable file written (CrowdStrike: ProcessRollup2, ElfFileWritten. SentinelOne: Process Creation, File Creation. Defender: DeviceProcessEvents, DeviceFileEvents) | HIGH | Q3, Q6, Q7 |
| H3 | The watchdog or startup hook launched svc.py from a campaign directory. | T1546.004, T1053.003 | Process creation with command line, image path and parent (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents) | HIGH | Q5 |
| H4 | A process presents as a kernel worker but carries userspace arguments, or executes from a memory file descriptor. | T1036, T1620 | Process creation with command line and image path (as above) | MEDIUM | Q4 |
| H5 | The host resolved a mining pool or campaign hostname. | T1496, T1071 | DNS lookup with requesting process (CrowdStrike: DnsRequest. SentinelOne: DNS Resolved. Defender: DeviceEvents DnsQueryResponse, confirm the JSON key in your tenant) | MEDIUM | Q2, Q12 |
| H6 | A Python parent spawned shell or persistence tooling touching rc files, authorized_keys or cron. | T1546.004, T1098.004, T1053.003 | Process creation with parent (as above). The in-process write path has no endpoint event. | LOW | Q8 |
| H7 | A container workload tried to reach the host disks or GPUStack's data directory. | T1611 | Process creation on the host sensor (as above) | MEDIUM | Q9 |
| H8 | Which of our hosts load model code with the remote-code switch at all? | T1195.001, T1059.006 | Process creation with command line (as above) | LOW | Q11 |
Attack chain
| Step | Behavior | Telemetry | Hunt angle |
|---|---|---|---|
| 1 Publish | Operator posts repositories named like small Qwen2 test checkpoints (qwen2-tiny and similar). Wave 1 on hhzz667, dsxk4mr8tq and a ModelScope mirror; wave 2 on MENXIANGYU, wjsbg, htt0118, soumikmahato, r1r13. | Outside endpoint telemetry (hub-side) | Approved-model list; review auto_map files before loading |
| 2 Load | Victim calls from_pretrained with trust_remote_code=True. Transformers imports the auto_map module; top-level code runs before the class is returned. | Process creation with command line (only when the flag is a CLI switch) | Q11 inventory |
| 3 Foothold | _boot() writes /tmp/.cch/svc.py, appends a nohup hook to /root/.bashrc, /root/.profile and ~/.bashrc. Wave 1 also wrote cron.d/.sync-helper and planted an SSH key in six paths. | Outside endpoint telemetry for in-process file writes | auditd (Section 7, 13); Q8 for shell-out variants |
| 4 Shell | Three wave-2 builds connect a shell to 212.32.253.3 on 443 or 9443 and retry on disconnect. | Outbound network connection (as above) | Q1, Q10 |
| 5 Fetch | Downloader fetches syscore.bin from the repository or a mirror, with proxy settings explicitly ignored. | DNS lookup; outbound connection | Q2, Q12 |
| 6 Launch | Binary copied to a memfd and run with fexecve as [kworker/u20:1]; fallback copy at /tmp/.cch/sync. | Process creation; executable written (fallback copy) | Q3, Q4, Q6, Q7 |
| 7 Hold | 120-second watchdog relaunches the miner; rival miners are killed; receipts are written to the local Xinference registry. | Process creation (watchdog relaunch) | Q5 |
| 8 Pivot | r1r13 collects GPU and Tencent Cloud metadata (exfil to 2.28.9.199:7000); qa-whl-1021 attempts a container-to-host escape. | Outbound connection; process creation on host | Q1, Q9 |
Consolidated IOC Table
Every value below was re-read from the saved Pluto snapshot, not from a search summary. Importable rows are in the CSV in Section 10; the wallet, login and key rows are strings to hunt, not Falcon IOC types.
| Type | Value | Confidence | Action | Context |
|---|---|---|---|---|
| IPv4 | 212.32.253.3 | HIGH | detect | Reverse-shell receiver (443, 9443) and payload host (8895, 8898); qwen2-tiny-5/-6/-9, zz_ext |
| IPv4 | 38.55.133.49 | MEDIUM | detect | zz_ext alternate second-stage host (8901, 9001); Pluto notes the C2 is now dead |
| IPv4 | 8.137.187.63 | HIGH | detect | Wave-1 Alibaba Cloud C2 (80, 443, 50053, 50054) |
| IPv4 | 103.97.176.121 | HIGH | detect | wjsbg host: m2.bin (8090), g20_reclaim.py (8899), pool port 3361 |
| IPv4 | 178.33.123.26 | MEDIUM | detect | wjsbg/qwen2-final pool address (3361, 3362) |
| IPv4 | 2.28.9.199 | HIGH | detect | r1r13 GPU and cloud-metadata exfil endpoint (7000) |
| Domain | hz2006.aihzcc.top | HIGH | detect | wjsbg g20_reclaim.py host |
| Domain | prl-us.kryptex.network | HIGH | detect | Wave-2 mining pool (8048); shared public pool service |
| Domain | prl-sg.kryptex.network | HIGH | detect | Wave-2 mining pool (8048); shared public pool service |
| Domain | pearl-hk.luckypool.io | HIGH | detect | Wave-1 mining pool (3361, 3956); shared public pool service |
| Domain | pool.hashvault.pro | HIGH | detect | Wave-1 mining pool (80, 443); shared public pool service |
| Domain | pearl-sg1.luckypool.io | MEDIUM | detect | soumikmahato qwen2-tiny-chat pool (3360) |
| Domain | de.pearl.herominers.com | MEDIUM | detect | soumikmahato qwen2-tiny-chat pool (1200) |
| Domain | grows-stocks-vatican-flood.trycloudflare.com | MEDIUM | detect | soumikmahato beacon receiver (/beacon) |
| SHA256 | 88f4b95a1c3d0a0d9638dc6e69f5bbc9ce0e1b92d069a0e404f60fa5397ee020 | HIGH | detect | Wave-2 syscore.bin (UPX with PEK! markers; 40625780 bytes) |
| SHA256 | 587e3c3be3c75eb6586d69b25adb4c4208c1cf7d39157946da543dd823117adc | HIGH | detect | Wave-1 syscore.bin (33.8 MB) |
| SHA256 | b0e1ae6d73d656b203514f498b59cbcf29f067edf6fbd3803a3de7d21960848d | HIGH | detect | Wave-1 sysx.bin (7.9 MB) |
| SHA256 | 5dc4b927fb91442a66a2e02c1636f629d4042a03a5c1523cd841d6ece66e0beb | HIGH | detect | wjsbg/zh-assets m2.bin miner payload (35702840 bytes) |
| SHA256 | 414b3cf7a288eaa592b0f03d85c7a6960dbdb855fcb684478b9def9faa95c21d | HIGH | detect | Wave-2 modeling_evil2.py loader (not an ELF; file-scan tools only) |
| SHA256 | 170ef5754bea51c3b05ae4b6c15c541ffa51d972bf8fe0bd3d1c3a19946bd6ac | HIGH | detect | htt0118/qwen2-tiny modeling_evil6.py loader |
| SHA256 | 2cd9328715bbffb6b090a58de17222c830d910f5ec8198e3fefb0fca2f3e0ef0 | HIGH | detect | wjsbg/qwen2-final modeling_qtz.py loader |
| SHA256 | 849ce59ffb2ae55e4131111acd8a511444475639ba18515509310414b1661964 | HIGH | detect | MENXIANGYU/qa-whl-1021 e.sh container-escape script |
| SHA256 | 6b1e531c9e799a6df1472acc143c6eaec6ea4dd22f997458ba519644d125197e | MEDIUM | detect | soumikmahato sysd-kit.tgz GPU miner and CUDA runtime archive |
| SHA256 | 9d62179c94faf0c8eb86704973c218c229e92c71c0ed60a5e85d4bcc69c13f40 | MEDIUM | detect | soumikmahato configuration_parasit.py |
| SHA256 | 6a837d3042b9b8b6266c75b07e22ea8e7a7108083efa48ff0aedba7fa24fb57c | MEDIUM | detect | soumikmahato modeling_parasit.py |
| Login | krxXJ2PKQW.worker | HIGH | hunt | Shared pool login across six analyzed wave-2 builds and htt0118/qwen2-tiny |
| Wallet | prl1p9s2a9gdy4n5yfuf5suhya2d2pyvaahmz3n3gzm3npyxkj79m7yyq9kgnrv | HIGH | hunt | Wave-1 payout wallet; live and victim-mined (about 2,276 PRL received) |
| Wallet | prl1pek8rh8yjck5q7596aph7ua7xnkqs8ur9mkfmz8w7xfzcdfcfx5vsx2f4lz | MEDIUM | hunt | soumikmahato/qwen2-tiny-chat wallet |
| Wallet | 83sB24CNcALNWZJrWb7zEnY6ZAtneXwfKAWyedqocXrd9cgzrLDcSdEch9xpHu6ZuqUdAh8VEMA15Zy6W5xMfZfPG4whgZj | MEDIUM | hunt | Wave-1 Monero wallet (dead) |
| SSH key comment | qx-5090-op | HIGH | hunt | Wave-1 ed25519 key planted in six authorized_keys paths |
Pool hostnames are shared public services. A hit proves mining on the host, not Load/Mine on its own: corroborate with the process, directory or hash queries.
Affected Surface & Telemetry Matrix
| Surface | Telemetry | Priority | Gap |
|---|---|---|---|
| Linux GPU and inference hosts (Xinference, GPUStack) | Process creation with command line and parent; executable written; outbound connection; DNS lookup (CrowdStrike: ProcessRollup2, ElfFileWritten, NetworkConnectIP4, DnsRequest. SentinelOne: Process Creation, File Creation, IP Connect, DNS Resolved. Defender: DeviceProcessEvents, DeviceFileEvents, DeviceNetworkEvents, DeviceEvents) | HIGH | Writes to shell startup files, cron and authorized_keys made in-process are outside endpoint telemetry. The memfd launch may record an unusual image path. |
| Containers on those hosts | Host-sensor view of container processes | HIGH | A container with no sensor is visible only through the host. Escape staging runs from inside the container. |
| Rented GPU machines, shared notebooks, CI jobs | Same Linux telemetry where a sensor is installed | MEDIUM | Ephemeral and third-party hosts often carry no sensor. Outside endpoint telemetry entirely. |
| Model-loading clients (any host calling from_pretrained) | Process creation with command line | MEDIUM | The flag set in Python source is outside endpoint telemetry; only a CLI switch shows. |
| Hub and registry side | None | MEDIUM | Outside endpoint telemetry. Control by allow-listing and pinning, Section 12. |
ATT&CK Mapping
| Tactic | Technique | Observed behavior | Query / Control |
|---|---|---|---|
| Initial Access | T1195.001 Compromise Software Dependencies and Development Tools | Malicious model repositories loaded with remote code enabled | Q11; Section 12 |
| Execution | T1059.006 Python | Module-level code runs on import via auto_map | Q3, Q5, Q11 |
| Persistence | T1546.004 Unix Shell Configuration Modification | nohup hook appended to .bashrc and .profile files | Q5, Q8; auditd |
| Persistence | T1098.004 SSH Authorized Keys | Wave-1 ed25519 key in six paths | Q8; Section 7 |
| Persistence | T1053.003 Cron | /etc/cron.d/.sync-helper (wave 1) | Q5, Q8; auditd |
| Defense Evasion | T1620 Reflective Code Loading | memfd_create plus fexecve launch | Q4; auditd syscall rule |
| Defense Evasion | T1036 Masquerading | Process named [kworker/u20:1] with userspace arguments | Q4 |
| Defense Evasion | T1027.002 Software Packing | UPX with markers altered to PEK! | Q7 |
| Defense Evasion | T1070.006 Timestomp | soumikmahato build forges timestamps | Section 7 (file times) |
| Command and Control | T1095 Non-Application Layer Protocol | Reconnecting reverse shell to 212.32.253.3 on 443 and 9443 | Q1, Q10 |
| Command and Control | T1105 Ingress Tool Transfer | syscore.bin and second stages fetched from repos, mirrors and 212.32.253.3 | Q1, Q2, Q12 |
| Collection / Discovery | T1552.005 Cloud Instance Metadata API | r1r13 collects Tencent Cloud metadata and GPU details | Q1 |
| Exfiltration | T1041 Exfiltration Over C2 Channel | Recon and metadata sent to 2.28.9.199:7000 and 212.32.253.3:8895 | Q1 |
| Privilege Escalation | T1611 Escape to Host | qa-whl-1021 activates LVM, mounts host disks, targets GPUStack data dir | Q9; Section 12 |
| Impact | T1496 Resource Hijacking | peakminer on victim GPUs paying the operator's wallets | Q2, Q3, Q7 |
Native Audit-Log Hunts
Non-CQL checks for the places Falcon is blind. Section 13 deploys the auditd rules that create the missing telemetry; until then these point-in-time checks are the only way to see the in-process file writes.
# Read-only checks. Run as an administrator on a suspect Linux host. Nothing here modifies the host. # 1. Campaign directories and the watchdog ls -la /tmp/.cch /tmp/.cchx /tmp/.kitsd /tmp/.c /tmp/.cache-hf 2>/dev/null ls -la /etc/cron.d/.sync-helper /tmp/.qa_pwned /root/qa_pwned 2>/dev/null # 2. Shell startup hooks that reference svc.py grep -n "svc.py" /root/.bashrc /root/.profile /home/*/.bashrc 2>/dev/null # 3. Processes carrying the shared pool login, or a kernel-worker name with arguments grep -al "krxXJ2PKQW" /proc/[0-9]*/cmdline 2>/dev/null ps -eo pid,ppid,user,args | grep -E "^ *[0-9]+ +[0-9]+ +[a-z0-9_-]+ +\[kworker/u[0-9]+:[0-9]+\] +[^ ]" # 4. Executables running from a deleted or memory-backed file ls -l /proc/[0-9]*/exe 2>/dev/null | grep -E "memfd:| \(deleted\)" # 5. Wave-1 SSH key comment in every authorized_keys on the host grep -rn "qx-5090-op" /root/.ssh /home/*/.ssh 2>/dev/null # 6. Established connections to the reverse-shell and payload host ss -tnp 2>/dev/null | grep -E "212[.]32[.]253[.]3" # 7. Transformers' dynamic-module cache shows which repositories' code was imported and when ls -laR ~/.cache/huggingface/modules/transformers_modules 2>/dev/null | head -200 ls -la ~/.cache/huggingface/hub 2>/dev/null | grep -E "models--(MENXIANGYU|wjsbg|htt0118|soumikmahato|r1r13|hhzz667|dsxk4mr8tq)--" # 8. Rival-miner and startup-entry deletions: the payload removes competitors, so review what is MISSING journalctl --since "30 days ago" 2>/dev/null | grep -i -E "miner|xmrig" | head -50
- Xinference registry. List the model registrations on the Xinference API or UI and look for names beginning
qwen2-tiny-orezv-followed by encoded text. Pluto documents these as the payload's progress receipts, written as fake model names. A missing registration that should exist is also a signal, because the payload removes rival registrations. - Proxy and DNS logs. Look for production or GPU hosts reaching
hf-mirror.comor a ModelScope API path, and for any direct outbound connection that bypasses the egress proxy (the downloader explicitly ignores proxy settings). - Hub-side. Compare every model your pipelines pull against the approved list. Repositories under MENXIANGYU, wjsbg, htt0118, soumikmahato, r1r13, hhzz667, dsxk4mr8tq and wssb123e are the campaign's.
- File times. The soumikmahato build forges timestamps, so do not date an infection from file times alone; cross-check with shell history, auditd and network logs.
Hunt Queries: CrowdStrike CQL, SentinelOne PowerQuery and Defender XDR KQL
Use your tenant's own address. SentinelOne does not document a link that pre-fills a query, so each "Copy & open SentinelOne" button copies the query and opens this console: paste it into PowerQuery.
Pick your portal. Microsoft documents a Share link in advanced hunting but not its URL format, so each "Copy & open Defender" button copies the query and opens Advanced hunting: paste it into the editor.
Linux telemetry is thin where this payload is strongest. The Falcon Linux sensor reports process creation, executable writes, DNS and network connections. It does not report writes to text files (shell startup files, cron, authorized_keys), and the memfd launch may be recorded with an unusual image path. These queries therefore target the process, directory, hash, DNS and network traces the sensor does see. Set the time window in the Falcon console.
Looks for: Outbound connections from a Linux host to the addresses the Pluto Security IOC table attributes to Load/Mine: the reverse-shell and payload host, the zz_ext alternate host, the wave-1 Alibaba Cloud C2, the wjsbg payload and pool hosts, and the r1r13 exfil endpoint. Run it estate-wide first: it is the cheapest answer to whether any host already talked to the operator.
Known limitations and deployment notes
- PowerQuery: A connection the agent did not record is not captured.
- PowerQuery assumes: dst.ip.address holds the remote address on IP Connect events from Linux agents.
- KQL – Defender XDR: A connection the sensor did not record is not captured.
- KQL – Defender XDR assumes: Linux sensors populate DeviceNetworkEvents for outbound connections.
// HUNT: Load/Mine campaign network infrastructure // MITRE: T1095, T1105, T1041 // CONF: high FP: low COST: low | REQUIRES: Falcon Linux sensor, NetworkConnectIP4 // FALSE POSITIVES: none expected for these addresses. A hit on a host that never loaded a remote-code model still needs triage. // Addresses: 212.32.253.3 (reverse shell 443/9443, payload 8895/8898), 38.55.133.49, 8.137.187.63, 103.97.176.121, 178.33.123.26, 2.28.9.199 #event_simpleName=NetworkConnectIP4 | in(field=RemoteAddressIP4, values=["212.32.253.3","38.55.133.49","8.137.187.63","103.97.176.121","178.33.123.26","2.28.9.199"]) | table([@timestamp, ComputerName, aid, ContextBaseFileName, ContextProcessId_decimal, LocalAddressIP4, RemoteAddressIP4, RemotePort], limit=max)
3 required fields · View requirements
Required telemetry
- IP Connect
Required fields
event.time: Event timeevent.type: Event typedst.ip.address: Remote address
Recommended fields
src.process.name: Connecting processdst.port.number: Remote port
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'IP Connect' dst.ip.address = * | limit 5 | columns event.time, event.type, dst.ip.address
// HUNT: Load/Mine campaign network infrastructure
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1095, T1105, T1041
// CONF: high | FP: low
// FALSE POSITIVES: none expected for these addresses. A hit on a host that never loaded a remote-code model still needs triage.
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: an outbound connection to one of the six campaign addresses. On IP Connect events the acting process is src.process.*.
// REQUIRES: SentinelOne IP Connect events from Linux agents
// TUNING: None needed for these addresses; triage any hit on a host that never loaded a remote-code model.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A connection the agent did not record is not captured.
// VALIDATION LEVEL: static-reviewed
event.type = 'IP Connect' dst.ip.address in ('212.32.253.3', '38.55.133.49', '8.137.187.63', '103.97.176.121', '178.33.123.26', '2.28.9.199')
| columns event.time, endpoint.name, agent.uuid, src.process.name, src.process.pid, src.ip.address, dst.ip.address, dst.port.number
| limit 1000Automated validation: static-passed
2 required fields · View requirements
Required telemetry
- DeviceNetworkEvents
Required fields
Timestamp: Event timeRemoteIP: Remote address
Recommended fields
InitiatingProcessFileName: Connecting processRemotePort: Remote port
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceNetworkEvents | where Timestamp > ago(1d) | project Timestamp, RemoteIP | take 5
// HUNT: Load/Mine campaign network infrastructure
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1095, T1105, T1041
// CONF: high | FP: low
// FALSE POSITIVES: none expected for these addresses. A hit on a host that never loaded a remote-code model still needs triage.
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: an outbound connection to one of the six campaign addresses, read from DeviceNetworkEvents RemoteIP.
// REQUIRES: Defender for Endpoint network telemetry from Linux sensors (DeviceNetworkEvents)
// TUNING: None needed for these addresses; triage any hit on a host that never loaded a remote-code model.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A connection the sensor did not record is not captured.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceNetworkEvents
| where Timestamp >= ago(7d)
| where RemoteIP in ("212.32.253.3", "38.55.133.49", "8.137.187.63", "103.97.176.121", "178.33.123.26", "2.28.9.199")
| project Timestamp, ReportId, DeviceId, DeviceName, InitiatingProcessFileName, InitiatingProcessId, LocalIP, RemoteIP, RemotePort
| sort by Timestamp desc
| take 200Looks for: DNS lookups for the pool hosts the loaders hard-code (Kryptex, luckypool, hashvault, herominers) and for the campaign-owned hz2006.aihzcc.top and trycloudflare beacon hostnames. Pool names are shared public services, so a hit proves mining, not Load/Mine specifically: pair it with Q3 to Q6 on the same host. FP: a sanctioned miner or a research workstation. Pool names are not unique to this campaign.
Known limitations and deployment notes
- PowerQuery: Hosts whose agent does not record DNS Resolved events are not covered.
- PowerQuery assumes: event.dns.request holds the queried name; some schema versions expose dns.request instead, so run the readiness test first.
- PowerQuery assumes: The acting process on a DNS Resolved event is src.process.*.
- KQL – Defender XDR: Hosts whose sensor does not record DnsQueryResponse are not covered.
- KQL – Defender XDR assumes: DnsQueryResponse events are recorded for Linux sensors and carry the name under DnsQueryString or QueryName in AdditionalFields.
// HUNT: Load/Mine mining-pool and campaign-host DNS // MITRE: T1496, T1071 // CONF: medium FP: medium COST: low | REQUIRES: Falcon Linux sensor, DnsRequest // FALSE POSITIVES: sanctioned mining, crypto research hosts // TUNING: server and inference host groups should never resolve mining pools. Exclude only named research hosts by aid after confirming ownership. // Pool hosts come from the Pluto IOC table (wave 2: kryptex; wave 1: luckypool, hashvault; soumikmahato line: luckypool, herominers) #event_simpleName=DnsRequest | DomainName=/^(prl-(us|sg)\.kryptex\.network|pearl-hk\.luckypool\.io|pearl-sg1\.luckypool\.io|pool\.hashvault\.pro|de\.pearl\.herominers\.com|hz2006\.aihzcc\.top|grows-stocks-vatican-flood\.trycloudflare\.com)$/i | groupBy([aid, ComputerName, DomainName, ContextBaseFileName], function=[count(as=Lookups), min(@timestamp, as=FirstSeen), max(@timestamp, as=LastSeen)], limit=max) | sort(Lookups, order=desc, limit=200)
3 required fields · View requirements
Required telemetry
- DNS Resolved
Required fields
event.time: Event timeevent.type: Event typeevent.dns.request: Queried name
Recommended fields
src.process.name: Resolving process
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'DNS Resolved' event.dns.request = * | limit 5 | columns event.time, event.type, event.dns.request
// HUNT: Load/Mine mining-pool and campaign-host DNS
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1496, T1071
// CONF: medium | FP: medium
// FALSE POSITIVES: sanctioned mining, crypto research hosts
// EQUIVALENCE: equivalent-with-schema-assumptions - Same DNS lookup of the eight mining-pool and campaign hosts, grouped per host, name and resolving process. in:anycase reproduces the CQL case-insensitive regex; the two kryptex region variants are spelled out.
// REQUIRES: SentinelOne DNS Resolved events from Linux agents
// TUNING: Server and inference host groups should never resolve mining pools. Exclude only named research hosts by agent.uuid after confirming ownership.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Hosts whose agent does not record DNS Resolved events are not covered.
// VALIDATION LEVEL: static-reviewed
event.type = 'DNS Resolved' event.dns.request in:anycase ('prl-us.kryptex.network', 'prl-sg.kryptex.network', 'pearl-hk.luckypool.io', 'pearl-sg1.luckypool.io', 'pool.hashvault.pro', 'de.pearl.herominers.com', 'hz2006.aihzcc.top', 'grows-stocks-vatican-flood.trycloudflare.com')
| group lookups=count(), first_seen=min(event.time), last_seen=max(event.time) by agent.uuid, endpoint.name, event.dns.request, src.process.name
| sort -lookups
| limit 200Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceEvents
Required fields
Timestamp: Event timeActionType: Action typeAdditionalFields: Query detail
Recommended fields
InitiatingProcessFileName: Resolving process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceEvents | where Timestamp > ago(1d) | where ActionType == "DnsQueryResponse" | project Timestamp, ActionType, AdditionalFields | take 5
// HUNT: Load/Mine mining-pool and campaign-host DNS
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1496, T1071
// CONF: medium | FP: medium
// FALSE POSITIVES: sanctioned mining, crypto research hosts
// EQUIVALENCE: equivalent-with-schema-assumptions - Same DNS lookup of the eight hosts, read from DeviceEvents DnsQueryResponse. in~ reproduces the CQL case-insensitive regex; the two kryptex region variants are spelled out.
// REQUIRES: Defender for Endpoint DNS telemetry on Linux sensors (DeviceEvents ActionType DnsQueryResponse)
// TUNING: Server and inference host groups should never resolve mining pools. Exclude only named research hosts by DeviceId after confirming ownership.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Hosts whose sensor does not record DnsQueryResponse are not covered.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0
DeviceEvents
| where Timestamp >= ago(7d)
| where ActionType == "DnsQueryResponse"
| extend Domain = tolower(tostring(coalesce(parse_json(AdditionalFields)["DnsQueryString"], parse_json(AdditionalFields)["QueryName"])))
| where Domain in~ ("prl-us.kryptex.network", "prl-sg.kryptex.network", "pearl-hk.luckypool.io", "pearl-sg1.luckypool.io", "pool.hashvault.pro", "de.pearl.herominers.com", "hz2006.aihzcc.top", "grows-stocks-vatican-flood.trycloudflare.com")
| summarize Lookups = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceId, DeviceName, Domain, InitiatingProcessFileName
| sort by Lookups desc
| take 200Looks for: Process creation where the image is syscore or sysx, or the command line carries the shared pool login, a campaign payout wallet, the SSH key comment, or a second-stage filename from this campaign's own IOC table. The pool login rides on the miner command line, which is exactly why the masqueraded process has a tell.
Known limitations and deployment notes
- PowerQuery: Synthetic process events are not a separate SentinelOne event type, so none is queried.
- PowerQuery assumes: tgt.process.image.sha256 is populated for Linux process creation.
- KQL – Defender XDR: A process the sensor did not observe starting is not captured.
- KQL – Defender XDR assumes: Wallet strings are matched case-insensitively.
// HUNT: Load/Mine binary names, pool login, wallets and second-stage filenames // MITRE: T1496, T1105, T1036 // CONF: high FP: low COST: medium | REQUIRES: Falcon Linux sensor, ProcessRollup2 // FALSE POSITIVES: an unrelated product that ships a binary literally named syscore. Check SHA256HashData against Q7 before escalating. // Strings: krxXJ2PKQW.worker (shared pool login), qx-5090-op (wave-1 SSH key comment), lf_py.py and kitsd_v12.sh (campaign second stages), three payout wallets #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | (FileName=/^(syscore|sysx)(\.bin)?$/i OR CommandLine=/krxXJ2PKQW\.worker|peakminer|syscore\.bin|sysx\.bin|lf_py\.py|kitsd_v12\.sh|qx-5090-op/i OR CommandLine=/prl1p9s2a9gdy4n5yfuf5suhya2d2pyvaahmz3n3gzm3npyxkj79m7yyq9kgnrv|prl1pek8rh8yjck5q7596aph7ua7xnkqs8ur9mkfmz8w7xfzcdfcfx5vsx2f4lz|83sB24CNcALNWZJrWb7zEnY6ZAtneXwfKAWyedqocXrd9cgzrLDcSdEch9xpHu6ZuqUdAh8VEMA15Zy6W5xMfZfPG4whgZj/) | table([@timestamp, ComputerName, aid, UserName, FileName, ImageFileName, ParentBaseFileName, CommandLine, SHA256HashData, TargetProcessId_decimal], limit=200)
4 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.time: Event timeevent.type: Event typetgt.process.name: Created process nametgt.process.cmdline: Created command line
Recommended fields
src.process.name: Creator process
Optional fields
tgt.process.image.sha256: Hash. Population varies by tenant.
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'Process Creation' tgt.process.cmdline = * | limit 5 | columns event.time, event.type, tgt.process.name, tgt.process.cmdline
// HUNT: Load/Mine binary names, pool login, wallets and second-stage filenames
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1496, T1105, T1036
// CONF: high | FP: low
// FALSE POSITIVES: an unrelated product that ships a binary literally named syscore. Check SHA256HashData against Q7 before escalating.
// EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: campaign binary names, or command-line markers (pool login, second-stage filenames, SSH key comment, three wallets). On Process Creation the new process is tgt.process.*. The wallets are matched case-sensitively as in the CQL; the other markers are case-insensitive.
// REQUIRES: SentinelOne Process Creation events from Linux agents
// TUNING: Check the SHA-256 against Q7 before escalating a hit on a product that ships a binary named syscore.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Synthetic process events are not a separate SentinelOne event type, so none is queried.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation'
(tgt.process.name in:anycase ('syscore', 'sysx', 'syscore.bin', 'sysx.bin') or tgt.process.cmdline contains ('krxXJ2PKQW.worker', 'peakminer', 'syscore.bin', 'sysx.bin', 'lf_py.py', 'kitsd_v12.sh', 'qx-5090-op') or tgt.process.cmdline contains:matchcase ('prl1p9s2a9gdy4n5yfuf5suhya2d2pyvaahmz3n3gzm3npyxkj79m7yyq9kgnrv', 'prl1pek8rh8yjck5q7596aph7ua7xnkqs8ur9mkfmz8w7xfzcdfcfx5vsx2f4lz', '83sB24CNcALNWZJrWb7zEnY6ZAtneXwfKAWyedqocXrd9cgzrLDcSdEch9xpHu6ZuqUdAh8VEMA15Zy6W5xMfZfPG4whgZj'))
| columns event.time, endpoint.name, agent.uuid, src.process.user, tgt.process.name, tgt.process.image.path, src.process.name, tgt.process.cmdline, tgt.process.image.sha256, tgt.process.pid
| limit 200Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFileName: Created process nameProcessCommandLine: Created command line
Recommended fields
InitiatingProcessFileName: Initiating process
Optional fields
SHA1: Hash. SHA256 is often unpopulated in DeviceProcessEvents; SHA1 is the safer pivot.
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, ProcessCommandLine | take 5
// HUNT: Load/Mine binary names, pool login, wallets and second-stage filenames
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1496, T1105, T1036
// CONF: high | FP: low
// FALSE POSITIVES: an unrelated product that ships a binary literally named syscore. Check SHA256HashData against Q7 before escalating.
// EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: campaign binary names or command-line markers. contains and in~ are case-insensitive, so the wallets are matched more loosely than the CQL, which matches them case-sensitively.
// REQUIRES: Defender for Endpoint process-creation telemetry from Linux sensors (DeviceProcessEvents)
// TUNING: Check the hash against Q7 before escalating a hit on a product that ships a binary named syscore.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A process the sensor did not observe starting is not captured.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where FileName in~ ("syscore", "sysx", "syscore.bin", "sysx.bin") or ProcessCommandLine contains "krxXJ2PKQW.worker" or ProcessCommandLine contains "peakminer" or ProcessCommandLine contains "syscore.bin" or ProcessCommandLine contains "sysx.bin" or ProcessCommandLine contains "lf_py.py" or ProcessCommandLine contains "kitsd_v12.sh" or ProcessCommandLine contains "qx-5090-op" or ProcessCommandLine contains "prl1p9s2a9gdy4n5yfuf5suhya2d2pyvaahmz3n3gzm3npyxkj79m7yyq9kgnrv" or ProcessCommandLine contains "prl1pek8rh8yjck5q7596aph7ua7xnkqs8ur9mkfmz8w7xfzcdfcfx5vsx2f4lz" or ProcessCommandLine contains "83sB24CNcALNWZJrWb7zEnY6ZAtneXwfKAWyedqocXrd9cgzrLDcSdEch9xpHu6ZuqUdAh8VEMA15Zy6W5xMfZfPG4whgZj"
| project Timestamp, ReportId, DeviceId, DeviceName, AccountName, FileName, FolderPath, InitiatingProcessFileName, ProcessCommandLine, SHA1, SHA256
| sort by Timestamp desc
| take 200Looks for: The miner is launched from a memory file descriptor and renamed [kworker/u20:1]. A real kernel thread has no userspace arguments and does not appear as a normal process-creation event, so a bracketed kworker name followed by arguments is a strong tell. The second arm looks for images that resolve to memfd or an fd path. Coverage here is thin by design: confirm in your tenant what ImageFileName the sensor records for a memfd launch before relying on the second arm. FP: container runtimes re-execute themselves from a memfd copy of their own binary (runc, crun). The query excludes those names; extend the exclusion for any other legitimate memfd user you baseline.
Known limitations and deployment notes
- PowerQuery: If the sensor records a resolved path instead of memfd for fexecve launches, the memory-backed arm returns nothing.
- PowerQuery assumes: tgt.process.image.path shows a memfd: or /proc/self/fd path for a memory-backed launch; validate on the tenant.
- PowerQuery assumes: The runc, crun and containerd-shim exclusion applies to the created process name.
- KQL – Defender XDR: If the sensor records a resolved path instead of memfd for fexecve launches, the memory-backed arm returns nothing.
- KQL – Defender XDR assumes: FolderPath shows a memfd: or /proc/self/fd path for a memory-backed launch; validate on the tenant.
// HUNT: Load/Mine kworker masquerade and memory-backed execution // MITRE: T1036, T1620 // CONF: medium FP: medium COST: low | REQUIRES: Falcon Linux sensor, ProcessRollup2. ⚠ validate in tenant: the ImageFileName the sensor records for a memfd/fexecve launch. // FALSE POSITIVES: runc/crun self-re-exec from a memfd copy, JIT runtimes that exec from anonymous files // TUNING: baseline the memfd arm for 7 days and add the legitimate FileName values to the exclusion below. The kworker-with-arguments arm needs no tuning. // Regexes use "." for the path separator so no slash needs escaping. #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | (CommandLine=/^\[kworker.u\d+:\d+\]\s+\S/ OR ImageFileName=/memfd:|proc.self.fd|proc.\d+.fd/i) | FileName!=/^(runc|crun|containerd-shim.*)$/i | table([@timestamp, ComputerName, aid, UserName, FileName, ImageFileName, ParentBaseFileName, CommandLine, SHA256HashData, TargetProcessId_decimal], limit=200)
5 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.time: Event timeevent.type: Event typetgt.process.name: Created process nametgt.process.cmdline: Created command linetgt.process.image.path: Created image path
Recommended fields
src.process.name: Creator process
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'Process Creation' tgt.process.image.path = * | limit 5 | columns event.time, event.type, tgt.process.name, tgt.process.cmdline, tgt.process.image.path
// HUNT: Load/Mine kworker masquerade and memory-backed execution
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1036, T1620
// CONF: medium | FP: medium
// FALSE POSITIVES: runc/crun self-re-exec from a memfd copy, JIT runtimes that exec from anonymous files
// EQUIVALENCE: approximate-analytic - The kworker-with-arguments arm is the same predicate. The memfd arm reads the created process image path, which the sensor may record differently for a memfd or fexecve launch; the CQL itself flags this as needing tenant validation.
// REQUIRES: SentinelOne Process Creation events from Linux agents
// TUNING: Baseline the memfd arm for 7 days and add the legitimate process names to the exclusion. The kworker-with-arguments arm needs no tuning.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: If the sensor records a resolved path instead of memfd for fexecve launches, the memory-backed arm returns nothing.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation'
(tgt.process.cmdline matches '^\\[kworker.u\\d+:\\d+\\]\\s+\\S' or tgt.process.image.path matches 'memfd:|proc.self.fd|proc.\\d+.fd')
!(tgt.process.name in:anycase ('runc', 'crun') or tgt.process.name matches '^containerd-shim')
| columns event.time, endpoint.name, agent.uuid, src.process.user, tgt.process.name, tgt.process.image.path, src.process.name, tgt.process.cmdline, tgt.process.image.sha256, tgt.process.pid
| limit 200Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFileName: Created process nameFolderPath: Created process pathProcessCommandLine: Created command line
Recommended fields
InitiatingProcessFileName: Initiating process
Optional fields
SHA1: Hash. SHA256 is often unpopulated in DeviceProcessEvents; SHA1 is the safer pivot.
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, FolderPath, ProcessCommandLine | take 5
// HUNT: Load/Mine kworker masquerade and memory-backed execution
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1036, T1620
// CONF: medium | FP: medium
// FALSE POSITIVES: runc/crun self-re-exec from a memfd copy, JIT runtimes that exec from anonymous files
// EQUIVALENCE: approximate-analytic - The kworker-with-arguments arm is the same predicate. The memfd arm reads FolderPath, which the sensor may record differently for a memfd or fexecve launch; the CQL flags this as needing tenant validation.
// REQUIRES: Defender for Endpoint process-creation telemetry from Linux sensors (DeviceProcessEvents)
// TUNING: Baseline the memfd arm for 7 days and add the legitimate process names to the exclusion. The kworker-with-arguments arm needs no tuning.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: If the sensor records a resolved path instead of memfd for fexecve launches, the memory-backed arm returns nothing.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where ProcessCommandLine matches regex @"^\[kworker.u\d+:\d+\]\s+\S" or FolderPath matches regex @"(?i)memfd:|proc.self.fd|proc.\d+.fd"
| where not(FileName in~ ("runc", "crun") or FileName startswith "containerd-shim")
| project Timestamp, ReportId, DeviceId, DeviceName, AccountName, FileName, FolderPath, InitiatingProcessFileName, ProcessCommandLine, SHA1, SHA256
| sort by Timestamp desc
| take 200Looks for: Process creation that references the campaign's own drop locations: the .cch watchdog directory (svc.py, sync, o), .cchx, .kitsd, the wave-1 Xinference cache install, the wjsbg reclaim script path, the soumikmahato kit archive, the cron persistence name and the QA marker. The shell-startup hook launches svc.py with nohup, so the next login or shell start re-creates the watchdog and shows up here.
Known limitations and deployment notes
- PowerQuery: A payload run only from memory, with no process creation, is not captured.
- PowerQuery: The dotted names c.kit.tar.gz and cache-hf.g20.py are reached through the separator wildcard and do not appear as literal strings in the query.
- PowerQuery assumes: matches is case-insensitive by default, which reproduces the CQL /i flag.
- PowerQuery assumes: The CQL's path-separator wildcard is kept, so c.kit.tar.gz and cache-hf.g20.py are matched the way the CQL's own regex matches them.
- KQL – Defender XDR: A payload run only from memory, with no process creation, is not captured.
- KQL – Defender XDR: The dotted names c.kit.tar.gz and cache-hf.g20.py are reached through the separator wildcard and do not appear as literal strings in the query.
- KQL – Defender XDR assumes: FolderPath holds the full image path of the created process.
- KQL – Defender XDR assumes: The (?i) prefix reproduces the CQL /i flag.
- KQL – Defender XDR assumes: The CQL's path-separator wildcard is kept, so c.kit.tar.gz and cache-hf.g20.py are matched the way the CQL's own regex matches them.
// HUNT: Load/Mine payload directories, watchdog and startup-hook execution // MITRE: T1546.004, T1053.003, T1105 // CONF: high FP: low COST: medium | REQUIRES: Falcon Linux sensor, ProcessRollup2 // FALSE POSITIVES: rare. A user directory literally named .cch. Review the parent before escalating. // Paths (Pluto IOC table): /tmp/.cch/{sync,svc.py,o}, /tmp/.cchx, /tmp/.kitsd, /root/.xinference/cache, /etc/cron.d/.sync-helper, /tmp/.qa_pwned, /tmp/.cache-hf/g20.py, /tmp/.c/kit.tar.gz // "." in the regexes stands for the path separator, so no slash is escaped. #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | (CommandLine=/\.cch.(svc\.py|sync|o)\b|\.cchx|\.kitsd|\.cache-hf.g20\.py|\.c.kit\.tar\.gz|\.sync-helper|\.qa_pwned|\.xinference.cache.(sync|syscore|sysx)/i OR ImageFileName=/\.cch.(sync|o)$|\.cchx|\.kitsd/i) | table([@timestamp, ComputerName, aid, UserName, FileName, ImageFileName, ParentBaseFileName, CommandLine, TargetProcessId_decimal], limit=200)
4 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.time: Event timeevent.type: Event typetgt.process.cmdline: Created command linetgt.process.image.path: Created image path
Recommended fields
src.process.name: Creator process
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'Process Creation' tgt.process.cmdline = * | limit 5 | columns event.time, event.type, tgt.process.cmdline, tgt.process.image.path
// HUNT: Load/Mine payload directories, watchdog and startup-hook execution // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1546.004, T1053.003, T1105 // CONF: high | FP: low // FALSE POSITIVES: rare. A user directory literally named .cch. Review the parent before escalating. // EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: a campaign payload path in the command line, or a campaign path as the created image. The path-separator wildcard is kept as a dot. // REQUIRES: SentinelOne Process Creation events from Linux agents // TUNING: Review the creator process before escalating a hit on a user directory that is legitimately named .cch. // LOOKBACK: 7d; extend to 30d for the incident window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A payload run only from memory, with no process creation, is not captured. The dotted names c.kit.tar.gz and cache-hf.g20.py are reached through the separator wildcard and do not appear as literal strings in the query. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' (tgt.process.cmdline matches '\\.cch.(svc\\.py|sync|o)\\b|\\.cchx|\\.kitsd|\\.cache-hf.g20\\.py|\\.c.kit\\.tar\\.gz|\\.sync-helper|\\.qa_pwned|\\.xinference.cache.(sync|syscore|sysx)' or tgt.process.image.path matches '\\.cch.(sync|o)$|\\.cchx|\\.kitsd') | columns event.time, endpoint.name, agent.uuid, src.process.user, tgt.process.name, tgt.process.image.path, src.process.name, tgt.process.cmdline, tgt.process.pid | limit 200
Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFolderPath: Created process pathProcessCommandLine: Created command line
Recommended fields
InitiatingProcessFileName: Initiating process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FolderPath, ProcessCommandLine | take 5
// HUNT: Load/Mine payload directories, watchdog and startup-hook execution // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1546.004, T1053.003, T1105 // CONF: high | FP: low // FALSE POSITIVES: rare. A user directory literally named .cch. Review the parent before escalating. // EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: a campaign payload path in the command line, or a campaign path as the created image. The path-separator wildcard is kept as a dot. // REQUIRES: Defender for Endpoint process-creation telemetry from Linux sensors (DeviceProcessEvents) // TUNING: Review the initiating process before escalating a hit on a user directory that is legitimately named .cch. // LOOKBACK: 7d; extend to 30d for the incident window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A payload run only from memory, with no process creation, is not captured. The dotted names c.kit.tar.gz and cache-hf.g20.py are reached through the separator wildcard and do not appear as literal strings in the query. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceProcessEvents | where Timestamp >= ago(7d) | where ProcessCommandLine matches regex @"(?i)\.cch.(svc\.py|sync|o)\b|\.cchx|\.kitsd|\.cache-hf.g20\.py|\.c.kit\.tar\.gz|\.sync-helper|\.qa_pwned|\.xinference.cache.(sync|syscore|sysx)" or FolderPath matches regex @"(?i)\.cch.(sync|o)$|\.cchx|\.kitsd" | project Timestamp, ReportId, DeviceId, DeviceName, AccountName, FileName, FolderPath, InitiatingProcessFileName, ProcessCommandLine | sort by Timestamp desc | take 200
Looks for: Linux executable-write telemetry for an ELF landing in a .cch, .cchx or .kitsd directory, in the wave-1 Xinference cache path, or named syscore or sysx. This is the one file-write event the Linux sensor reliably gives you here: it fires on executables, not on text files, so shell startup files, cron entries and authorized_keys are NOT covered by it (see the Coverage Map).
Known limitations and deployment notes
- PowerQuery: The CQL reports only executables; this pivot also returns scripts and other files in the same paths.
- PowerQuery assumes: No attested field marks a written file as an ELF executable, so the executable filter is not applied.
- PowerQuery assumes: The acting process on a file event is src.process.*.
- KQL – Defender XDR: The CQL reports only executables; this pivot also returns scripts and other files in the same paths.
- KQL – Defender XDR assumes: FolderPath holds the directory or full path of the written file.
- KQL – Defender XDR assumes: No column marks a written file as an ELF executable, so the executable filter is not applied.
// HUNT: Load/Mine ELF written to campaign paths or campaign binary names // MITRE: T1105, T1036 // CONF: high FP: low COST: low | REQUIRES: Falcon Linux sensor, ElfFileWritten // FALSE POSITIVES: none expected. Only executables are reported, never .py or rc files. // Event name per crowdstrike-logscale-v3 references/falcon-events.md (ElfFileWritten). The /i flag makes the match case-insensitive so a tenant that spells it ELFFileWritten still matches. #event_simpleName=/^ElfFileWritten$/i | (TargetFileName=/\.cch|\.cchx|\.kitsd|\.xinference.cache/i OR FileName=/^(syscore|sysx)(\.bin)?$/i) | table([@timestamp, ComputerName, aid, FileName, TargetFileName, SHA256HashData, ContextProcessId_decimal], limit=200)
3 required fields · View requirements
Required telemetry
- File Creation
- File Modification
Required fields
event.time: Event timeevent.type: Event typetgt.file.path: Written file path
Recommended fields
src.process.name: Writing process
Optional fields
tgt.file.sha256: Hash. Population varies by tenant.
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'File Creation' tgt.file.path = * | limit 5 | columns event.time, event.type, tgt.file.path
// HUNT: Load/Mine ELF written to campaign paths or campaign binary names
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1105, T1036
// CONF: high | FP: low
// FALSE POSITIVES: none expected. Only executables are reported, never .py or rc files.
// EQUIVALENCE: approximate-analytic - SentinelOne has no ELF-specific write event, so the pivot is any file creation or modification at a campaign path or under a campaign binary name. It is broader than the CQL, which reports executables only.
// REQUIRES: SentinelOne File Creation and File Modification events from Linux agents
// TUNING: Expect extra rows for non-executable files (scripts, config) written to the campaign directories; those are still campaign artifacts.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: The CQL reports only executables; this pivot also returns scripts and other files in the same paths.
// VALIDATION LEVEL: static-reviewed
event.type in ('File Creation', 'File Modification')
(tgt.file.path matches '\\.cch|\\.cchx|\\.kitsd|\\.xinference.cache' or tgt.file.path matches '/(syscore|sysx)(\\.bin)?$')
| columns event.time, endpoint.name, agent.uuid, event.type, src.process.name, tgt.file.path, tgt.file.sha256, src.process.pid
| limit 200Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceFileEvents
Required fields
Timestamp: Event timeActionType: Action typeFolderPath: Written file pathFileName: Written file name
Recommended fields
InitiatingProcessFileName: Writing process
Optional fields
SHA1: Hash. SHA256 is often unpopulated; SHA1 is the safer pivot.
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceFileEvents | where Timestamp > ago(1d) | project Timestamp, ActionType, FolderPath, FileName | take 5
// HUNT: Load/Mine ELF written to campaign paths or campaign binary names
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1105, T1036
// CONF: high | FP: low
// FALSE POSITIVES: none expected. Only executables are reported, never .py or rc files.
// EQUIVALENCE: approximate-analytic - Defender has no ELF-specific write event, so the pivot is any file creation or modification at a campaign path or under a campaign binary name. It is broader than the CQL, which reports executables only.
// REQUIRES: Defender for Endpoint file telemetry from Linux sensors (DeviceFileEvents)
// TUNING: Expect extra rows for non-executable files (scripts, config) written to the campaign directories; those are still campaign artifacts.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: The CQL reports only executables; this pivot also returns scripts and other files in the same paths.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceFileEvents
| where Timestamp >= ago(7d)
| where ActionType in ("FileCreated", "FileModified")
| where FolderPath matches regex @"(?i)\.cch|\.cchx|\.kitsd|\.xinference.cache" or FileName matches regex @"(?i)^(syscore|sysx)(\.bin)?$"
| project Timestamp, ReportId, DeviceId, DeviceName, ActionType, FileName, FolderPath, SHA1, SHA256, InitiatingProcessFileName
| sort by Timestamp desc
| take 200Looks for: Exact SHA-256 match for the four binaries the Pluto IOC table hashes: wave-2 syscore.bin, wave-1 syscore.bin, wave-1 sysx.bin and the wjsbg m2.bin. The loader modules (.py) are not executables in Falcon's eyes and will not hash here; they are in the IOC CSV for file-scanning tools. Hashes are single-origin (Pluto), so this is a detect, not a block.
Known limitations and deployment notes
- PowerQuery: A binary the agent did not hash is not matched.
- PowerQuery assumes: tgt.process.image.sha256 and tgt.file.sha256 are populated on the tenant for Linux agents.
- KQL – Defender XDR: The ELF-write arm is not covered, because a second source table is not allowed in one query.
- KQL – Defender XDR: SHA256 is usually unpopulated in DeviceProcessEvents, so this pivot can return nothing on a healthy tenant.
- KQL – Defender XDR: No SHA1 values exist in the source indicators.
- KQL – Defender XDR assumes: SHA256 is populated in DeviceProcessEvents on the tenant.
// HUNT: Load/Mine known miner binary hashes // MITRE: T1496, T1027.002 // CONF: high FP: low COST: low | REQUIRES: Falcon Linux sensor, ProcessRollup2 or ElfFileWritten with SHA256HashData // FALSE POSITIVES: none expected for an exact hash // Hashes: wave-2 syscore.bin, wave-1 syscore.bin, wave-1 sysx.bin, wjsbg m2.bin (all from the Pluto IOC table) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2|ElfFileWritten)$/i | in(field=SHA256HashData, values=["88f4b95a1c3d0a0d9638dc6e69f5bbc9ce0e1b92d069a0e404f60fa5397ee020","587e3c3be3c75eb6586d69b25adb4c4208c1cf7d39157946da543dd823117adc","b0e1ae6d73d656b203514f498b59cbcf29f067edf6fbd3803a3de7d21960848d","5dc4b927fb91442a66a2e02c1636f629d4042a03a5c1523cd841d6ece66e0beb"]) | table([@timestamp, ComputerName, aid, #event_simpleName, FileName, ImageFileName, TargetFileName, SHA256HashData, CommandLine], limit=max)
4 required fields · View requirements
Required telemetry
- Process Creation
- File Creation
- File Modification
Required fields
event.time: Event timeevent.type: Event typetgt.process.image.sha256: Created image hashtgt.file.sha256: Written file hash
Recommended fields
src.process.name: Creator process
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'Process Creation' tgt.process.image.sha256 = * | limit 5 | columns event.time, event.type, tgt.process.image.sha256, tgt.file.sha256
// HUNT: Load/Mine known miner binary hashes
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1496, T1027.002
// CONF: high | FP: low
// FALSE POSITIVES: none expected for an exact hash
// EQUIVALENCE: equivalent-with-schema-assumptions - Same exact-hash predicate across process creation and file writes: the SHA-256 of the created image or of the written file is one of the four known miner binaries.
// REQUIRES: SentinelOne Process Creation and file events with SHA-256 populated
// TUNING: None needed for exact hashes.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A binary the agent did not hash is not matched.
// VALIDATION LEVEL: static-reviewed
event.type in ('Process Creation', 'File Creation', 'File Modification')
(tgt.process.image.sha256 in:anycase ('88f4b95a1c3d0a0d9638dc6e69f5bbc9ce0e1b92d069a0e404f60fa5397ee020', '587e3c3be3c75eb6586d69b25adb4c4208c1cf7d39157946da543dd823117adc', 'b0e1ae6d73d656b203514f498b59cbcf29f067edf6fbd3803a3de7d21960848d', '5dc4b927fb91442a66a2e02c1636f629d4042a03a5c1523cd841d6ece66e0beb') or tgt.file.sha256 in:anycase ('88f4b95a1c3d0a0d9638dc6e69f5bbc9ce0e1b92d069a0e404f60fa5397ee020', '587e3c3be3c75eb6586d69b25adb4c4208c1cf7d39157946da543dd823117adc', 'b0e1ae6d73d656b203514f498b59cbcf29f067edf6fbd3803a3de7d21960848d', '5dc4b927fb91442a66a2e02c1636f629d4042a03a5c1523cd841d6ece66e0beb'))
| columns event.time, endpoint.name, agent.uuid, event.type, src.process.name, tgt.process.name, tgt.process.image.path, tgt.file.path, tgt.process.image.sha256, tgt.file.sha256, tgt.process.cmdline
| limit 1000Automated validation: static-passed
2 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeSHA256: Created image hash
Recommended fields
FileName: Created process name
Optional fields
SHA1: Hash. SHA256 is often unpopulated in DeviceProcessEvents; SHA1 is the safer pivot.
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, SHA256 | take 5
// HUNT: Load/Mine known miner binary hashes
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1496, T1027.002
// CONF: high | FP: low
// FALSE POSITIVES: none expected for an exact hash
// EQUIVALENCE: telemetry-gap - Partial pivot: only the process-creation arm is covered, on SHA256, which Microsoft documents as usually unpopulated. The CQL carries SHA-256 only, so no SHA1 pivot can be written.
// REQUIRES: Defender for Endpoint process-creation telemetry with SHA256 populated (DeviceProcessEvents)
// TUNING: Check population first: if SHA256 is empty on the tenant this pivot returns nothing, and the CQL tab is the reference.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: The ELF-write arm is not covered, because a second source table is not allowed in one query. SHA256 is usually unpopulated in DeviceProcessEvents, so this pivot can return nothing on a healthy tenant. No SHA1 values exist in the source indicators.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where SHA256 in~ ("88f4b95a1c3d0a0d9638dc6e69f5bbc9ce0e1b92d069a0e404f60fa5397ee020", "587e3c3be3c75eb6586d69b25adb4c4208c1cf7d39157946da543dd823117adc", "b0e1ae6d73d656b203514f498b59cbcf29f067edf6fbd3803a3de7d21960848d", "5dc4b927fb91442a66a2e02c1636f629d4042a03a5c1523cd841d6ece66e0beb")
| project Timestamp, ReportId, DeviceId, DeviceName, FileName, FolderPath, InitiatingProcessFileName, ProcessCommandLine, SHA1, SHA256
| sort by Timestamp desc
| take 200Looks for: A Python process spawning a shell or utility whose command line touches shell startup files, authorized_keys or cron.d. The documented payload writes those files from inside Python with no child process, so this query will usually stay silent on the main path. It exists for the variants and follow-on stages that shell out. Do not read silence here as clean: see the Coverage Map gap. FP: configuration management, cloud-init and notebook extensions that edit rc files from a Python parent.
Known limitations and deployment notes
- PowerQuery: In-process edits of authorized_keys, .bashrc and cron files by the Python loader create no child process and are not seen.
- PowerQuery assumes: matches is case-insensitive by default, which reproduces the CQL /i flag.
- KQL – Defender XDR: In-process edits of authorized_keys, .bashrc and cron files by the Python loader create no child process and are not seen.
- KQL – Defender XDR assumes: InitiatingProcessFileName holds the Python interpreter name without a path.
// HUNT: Load/Mine persistence tooling spawned by a Python parent // MITRE: T1546.004, T1098.004, T1053.003 // CONF: low FP: medium COST: low | REQUIRES: Falcon Linux sensor, ProcessRollup2 // FALSE POSITIVES: Ansible/Salt/cloud-init run through Python, notebook extensions that edit .bashrc // TUNING: exclude known config-management parents by adding a ParentBaseFileName exclusion after you baseline 14 days on the GPU host group. // The main payload path edits these files in-process (no child process), so a quiet result here is NOT evidence of absence. #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^python([0-9.]*)$/i | FileName=/^(sh|bash|dash|crontab|tee|cp|mv|chmod|chattr|echo|sed)$/i | CommandLine=/authorized_keys|\.bashrc|\.profile|cron\.d|crontab/i | table([@timestamp, ComputerName, aid, UserName, FileName, ParentBaseFileName, CommandLine, TargetProcessId_decimal], limit=200)
5 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.time: Event timeevent.type: Event typesrc.process.name: Creator processtgt.process.name: Created process nametgt.process.cmdline: Created command line
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'Process Creation' tgt.process.cmdline = * | limit 5 | columns event.time, event.type, src.process.name, tgt.process.name, tgt.process.cmdline
// HUNT: Load/Mine persistence tooling spawned by a Python parent
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1546.004, T1098.004, T1053.003
// CONF: low | FP: medium
// FALSE POSITIVES: Ansible/Salt/cloud-init run through Python, notebook extensions that edit .bashrc
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a Python process creates a shell or file-editing tool whose command line touches a persistence file. On Process Creation the parent is src.process.* and the child is tgt.process.*.
// REQUIRES: SentinelOne Process Creation events from Linux agents
// TUNING: Exclude known config-management parents by process name after baselining 14 days on the GPU host group. A quiet result is not evidence of absence: the main payload edits these files in-process.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: In-process edits of authorized_keys, .bashrc and cron files by the Python loader create no child process and are not seen.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation'
src.process.name matches '^python([0-9.]*)$'
tgt.process.name in:anycase ('sh', 'bash', 'dash', 'crontab', 'tee', 'cp', 'mv', 'chmod', 'chattr', 'echo', 'sed')
tgt.process.cmdline matches 'authorized_keys|\\.bashrc|\\.profile|cron\\.d|crontab'
| columns event.time, endpoint.name, agent.uuid, src.process.user, tgt.process.name, src.process.name, tgt.process.cmdline, tgt.process.pid
| limit 200Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeInitiatingProcessFileName: Initiating processFileName: Created process nameProcessCommandLine: Created command line
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, InitiatingProcessFileName, ProcessCommandLine | take 5
// HUNT: Load/Mine persistence tooling spawned by a Python parent
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1546.004, T1098.004, T1053.003
// CONF: low | FP: medium
// FALSE POSITIVES: Ansible/Salt/cloud-init run through Python, notebook extensions that edit .bashrc
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a Python initiating process creates a shell or file-editing tool whose command line touches a persistence file.
// REQUIRES: Defender for Endpoint process-creation telemetry from Linux sensors (DeviceProcessEvents)
// TUNING: Exclude known config-management parents by InitiatingProcessFileName after baselining 14 days on the GPU host group. A quiet result is not evidence of absence.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: In-process edits of authorized_keys, .bashrc and cron files by the Python loader create no child process and are not seen.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where InitiatingProcessFileName matches regex @"(?i)^python([0-9.]*)$"
| where FileName in~ ("sh", "bash", "dash", "crontab", "tee", "cp", "mv", "chmod", "chattr", "echo", "sed")
| where ProcessCommandLine matches regex @"(?i)authorized_keys|\.bashrc|\.profile|cron\.d|crontab"
| project Timestamp, ReportId, DeviceId, DeviceName, AccountName, FileName, InitiatingProcessFileName, ProcessCommandLine
| sort by Timestamp desc
| take 200Looks for: The escape script described in the writeup activates LVM volumes, mounts candidate host disks under /mnt/qa_<pid>, and looks for the host root or GPUStack's data directory. This query catches a mount path named qa_ plus a number, or LVM activation tools launched by a Python parent. It runs on the host sensor: a container with no sensor of its own is only visible when its processes surface on the host. FP: storage automation written in Python that legitimately activates volume groups.
Known limitations and deployment notes
- PowerQuery: Escape staging performed from inside a container without an agent is not seen.
- PowerQuery assumes: matches is case-insensitive by default, which reproduces the CQL /i flag.
- KQL – Defender XDR: Escape staging performed from inside a container without a sensor is not seen.
- KQL – Defender XDR assumes: InitiatingProcessFileName holds the Python interpreter name without a path.
// HUNT: Load/Mine container-to-host escape staging // MITRE: T1611 // CONF: medium FP: medium COST: low | REQUIRES: Falcon Linux sensor on the host, ProcessRollup2 // FALSE POSITIVES: Python storage automation, backup agents that activate LVM // TUNING: the first arm (mount path qa_ plus digits) is campaign-specific and needs no tuning. For the LVM arm exclude your storage-automation parent by name after baselining. // "." in the regex stands for the path separator. #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | (CommandLine=/mnt.qa_\d+/i OR (FileName=/^(vgchange|lvchange|vgscan|lvscan|pvscan)$/i AND ParentBaseFileName=/^python([0-9.]*)$/i)) | table([@timestamp, ComputerName, aid, UserName, FileName, ParentBaseFileName, CommandLine, TargetProcessId_decimal], limit=200)
5 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.time: Event timeevent.type: Event typesrc.process.name: Creator processtgt.process.name: Created process nametgt.process.cmdline: Created command line
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'Process Creation' tgt.process.cmdline = * | limit 5 | columns event.time, event.type, src.process.name, tgt.process.name, tgt.process.cmdline
// HUNT: Load/Mine container-to-host escape staging
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1611
// CONF: medium | FP: medium
// FALSE POSITIVES: Python storage automation, backup agents that activate LVM
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a mount path of the qa_ plus digits form, or an LVM activation tool created by a Python parent. On Process Creation the parent is src.process.* and the child is tgt.process.*.
// REQUIRES: SentinelOne Process Creation events from Linux hosts (not inside the container)
// TUNING: The mount-path arm is campaign-specific and needs no tuning. For the LVM arm exclude your storage-automation parent by name after baselining.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Escape staging performed from inside a container without an agent is not seen.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation'
(tgt.process.cmdline matches 'mnt.qa_\\d+' or (tgt.process.name in:anycase ('vgchange', 'lvchange', 'vgscan', 'lvscan', 'pvscan') and src.process.name matches '^python([0-9.]*)$'))
| columns event.time, endpoint.name, agent.uuid, src.process.user, tgt.process.name, src.process.name, tgt.process.cmdline, tgt.process.pid
| limit 200Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeInitiatingProcessFileName: Initiating processFileName: Created process nameProcessCommandLine: Created command line
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, InitiatingProcessFileName, ProcessCommandLine | take 5
// HUNT: Load/Mine container-to-host escape staging
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1611
// CONF: medium | FP: medium
// FALSE POSITIVES: Python storage automation, backup agents that activate LVM
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a mount path of the qa_ plus digits form, or an LVM activation tool started by a Python initiating process.
// REQUIRES: Defender for Endpoint process-creation telemetry from Linux hosts (DeviceProcessEvents)
// TUNING: The mount-path arm is campaign-specific and needs no tuning. For the LVM arm exclude your storage-automation parent by InitiatingProcessFileName after baselining.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Escape staging performed from inside a container without a sensor is not seen.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where ProcessCommandLine matches regex @"(?i)mnt.qa_\d+" or (FileName in~ ("vgchange", "lvchange", "vgscan", "lvscan", "pvscan") and InitiatingProcessFileName matches regex @"(?i)^python([0-9.]*)$")
| project Timestamp, ReportId, DeviceId, DeviceName, AccountName, FileName, InitiatingProcessFileName, ProcessCommandLine
| sort by Timestamp desc
| take 200Looks for: Behavioural cover for the reverse shell and second-stage ports when the operator rotates addresses: a shell, Python or a downloader connecting to ports 7000, 8895, 8898, 8901, 9001 or 9443 on a non-private address. Ports 443 is deliberately left out because every Python process uses it. The sensor attributes the connection to the process that owns the socket, which may be Python rather than the shell it later spawns. FP: ports 7000 and 9001 are common for databases, MinIO and Tor. Expect hits from data tooling.
Known limitations and deployment notes
- PowerQuery: Connections made by other interpreters or by the miner binary itself are not matched.
- PowerQuery assumes: dst.port.number is numeric on IP Connect events.
- KQL – Defender XDR: Connections made by other interpreters or by the miner binary itself are not matched.
- KQL – Defender XDR assumes: RemoteIPType is populated, and Public excludes the same private, loopback and link-local ranges as the CQL.
// HUNT: Load/Mine campaign port set from shells and interpreters // MITRE: T1095, T1571, T1041 // CONF: medium FP: medium COST: low | REQUIRES: Falcon Linux sensor, NetworkConnectIP4 // FALSE POSITIVES: databases and object stores on 7000/9001, data pipelines on 8895/8898 // TUNING: exclude your own service subnets and known data-platform destinations by RemoteAddressIP4 once baselined. // Ports from the Pluto IOC table: 9443 (reverse shell), 8895/8898 (212.32.253.3), 8901/9001 (38.55.133.49), 7000 (2.28.9.199) #event_simpleName=NetworkConnectIP4 | ContextBaseFileName=/^(sh|bash|dash|zsh|python([0-9.]*)|curl|wget)$/i | RemotePort=/^(7000|8895|8898|8901|9001|9443)$/ | !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16","127.0.0.0/8","169.254.0.0/16"]) | table([@timestamp, ComputerName, aid, ContextBaseFileName, ContextProcessId_decimal, RemoteAddressIP4, RemotePort], limit=200)
5 required fields · View requirements
Required telemetry
- IP Connect
Required fields
event.time: Event timeevent.type: Event typesrc.process.name: Connecting processdst.port.number: Remote portdst.ip.address: Remote address
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'IP Connect' dst.ip.address = * | limit 5 | columns event.time, event.type, src.process.name, dst.ip.address, dst.port.number
// HUNT: Load/Mine campaign port set from shells and interpreters // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1095, T1571, T1041 // CONF: medium | FP: medium // FALSE POSITIVES: databases and object stores on 7000/9001, data pipelines on 8895/8898 // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a shell, Python, curl or wget process connects to one of the six campaign ports on a non-private, non-loopback, non-link-local address. On IP Connect events the acting process is src.process.*. // REQUIRES: SentinelOne IP Connect events from Linux agents // TUNING: Exclude your own service subnets and known data-platform destinations by dst.ip.address once baselined. // LOOKBACK: 7d; extend to 30d for the incident window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Connections made by other interpreters or by the miner binary itself are not matched. // VALIDATION LEVEL: static-reviewed event.type = 'IP Connect' src.process.name matches '^(sh|bash|dash|zsh|python([0-9.]*)|curl|wget)$' dst.port.number in (7000, 8895, 8898, 8901, 9001, 9443) !(net_rfc1918(dst.ip.address) or net_ipsubnet(dst.ip.address, '127.0.0.0/8') or net_ipsubnet(dst.ip.address, '169.254.0.0/16')) | columns event.time, endpoint.name, agent.uuid, src.process.name, src.process.pid, dst.ip.address, dst.port.number | limit 200
Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceNetworkEvents
Required fields
Timestamp: Event timeInitiatingProcessFileName: Connecting processRemotePort: Remote portRemoteIPType: Remote address class
Recommended fields
RemoteIP: Remote address
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceNetworkEvents | where Timestamp > ago(1d) | project Timestamp, InitiatingProcessFileName, RemotePort, RemoteIPType | take 5
// HUNT: Load/Mine campaign port set from shells and interpreters // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1095, T1571, T1041 // CONF: medium | FP: medium // FALSE POSITIVES: databases and object stores on 7000/9001, data pipelines on 8895/8898 // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a shell, Python, curl or wget process connects to one of the six campaign ports on a public address. RemoteIPType Public replaces the CQL private, loopback and link-local exclusion list. // REQUIRES: Defender for Endpoint network telemetry from Linux sensors (DeviceNetworkEvents) // TUNING: Exclude your own service subnets and known data-platform destinations by RemoteIP once baselined. // LOOKBACK: 7d; extend to 30d for the incident window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Connections made by other interpreters or by the miner binary itself are not matched. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceNetworkEvents | where Timestamp >= ago(7d) | where InitiatingProcessFileName matches regex @"(?i)^(sh|bash|dash|zsh|python([0-9.]*)|curl|wget)$" | where RemotePort in (7000, 8895, 8898, 8901, 9001, 9443) | where RemoteIPType == "Public" | project Timestamp, ReportId, DeviceId, DeviceName, InitiatingProcessFileName, InitiatingProcessId, RemoteIP, RemotePort | sort by Timestamp desc | take 200
Looks for: Not a detection. It lists Linux hosts whose process command lines carry a trust-remote-code switch, which is where this campaign's code would execute. Use it to scope the loading surface, then review which repositories those hosts pull. Code that sets the flag in Python source rather than on the command line is invisible here. FP: every legitimate use of the flag appears. That is the point of the inventory.
Known limitations and deployment notes
- PowerQuery: Loaders started with the flag set in a script file rather than on the command line are not seen.
- PowerQuery assumes: matches is case-insensitive by default, which reproduces the CQL /i flag.
- KQL – Defender XDR: Loaders started with the flag set in a script file rather than on the command line are not seen.
- KQL – Defender XDR assumes: The (?i) prefix reproduces the CQL /i flag.
// HUNT: exposure inventory, model loaders launched with trust-remote-code // MITRE: T1195.001, T1059.006 // CONF: low FP: high COST: low | REQUIRES: Falcon Linux sensor, ProcessRollup2 // FALSE POSITIVES: all sanctioned use of the flag // TUNING: this is an inventory, not an alert. Review each host's repositories against your approved-model list; do not schedule it as a detection. #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | CommandLine=/trust[-_]remote[-_]code/i | groupBy([aid, ComputerName, FileName], function=[count(as=Launches), max(@timestamp, as=LastSeen)], limit=max) | sort(Launches, order=desc, limit=200)
4 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.time: Event timeevent.type: Event typetgt.process.name: Created process nametgt.process.cmdline: Created command line
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'Process Creation' tgt.process.cmdline = * | limit 5 | columns event.time, event.type, tgt.process.name, tgt.process.cmdline
// HUNT: exposure inventory, model loaders launched with trust-remote-code // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1195.001, T1059.006 // CONF: low | FP: high // FALSE POSITIVES: all sanctioned use of the flag // EQUIVALENCE: equivalent-with-schema-assumptions - Same inventory: process creations whose command line carries the trust-remote-code flag, counted per host and process name. // REQUIRES: SentinelOne Process Creation events from Linux agents // TUNING: This is an inventory, not an alert. Review each host's repositories against your approved-model list; do not schedule it as a detection. // LOOKBACK: 7d; extend to 30d for the incident window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Loaders started with the flag set in a script file rather than on the command line are not seen. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' tgt.process.cmdline matches 'trust[-_]remote[-_]code' | group launches=count(), last_seen=max(event.time) by agent.uuid, endpoint.name, tgt.process.name | sort -launches | limit 200
Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFileName: Created process nameProcessCommandLine: Created command line
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, ProcessCommandLine | take 5
// HUNT: exposure inventory, model loaders launched with trust-remote-code // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1195.001, T1059.006 // CONF: low | FP: high // FALSE POSITIVES: all sanctioned use of the flag // EQUIVALENCE: equivalent-with-schema-assumptions - Same inventory: process creations whose command line carries the trust-remote-code flag, counted per host and process name. // REQUIRES: Defender for Endpoint process-creation telemetry from Linux sensors (DeviceProcessEvents) // TUNING: This is an inventory, not an alert. Review each host's repositories against your approved-model list; do not schedule it as a detection. // LOOKBACK: 7d; extend to 30d for the incident window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Loaders started with the flag set in a script file rather than on the command line are not seen. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0 DeviceProcessEvents | where Timestamp >= ago(7d) | where ProcessCommandLine matches regex @"(?i)trust[-_]remote[-_]code" | summarize Launches = count(), LastSeen = max(Timestamp) by DeviceId, DeviceName, FileName | sort by Launches desc | take 200
Looks for: The flagship loader falls back to hf-mirror.com and wave 1 used a ModelScope mirror for the binary. Both are legitimate services, so this is a scoping hunt: which production or GPU hosts resolve them at all, and which process asked. FP: teams that legitimately use these mirrors, particularly in regions where the primary hub is slow.
Known limitations and deployment notes
- PowerQuery: Hosts whose agent does not record DNS Resolved events are not covered.
- PowerQuery assumes: event.dns.request holds the queried name; some schema versions expose dns.request instead, so run the readiness test first.
- PowerQuery assumes: The acting process on a DNS Resolved event is src.process.*.
- KQL – Defender XDR: Hosts whose sensor does not record DnsQueryResponse are not covered.
- KQL – Defender XDR assumes: DnsQueryResponse events are recorded for Linux sensors and carry the name under DnsQueryString or QueryName in AdditionalFields.
// HUNT: hosts resolving the mirrors used as binary fallbacks // MITRE: T1105 // CONF: low FP: medium COST: low | REQUIRES: Falcon Linux sensor, DnsRequest // FALSE POSITIVES: sanctioned use of hf-mirror.com or ModelScope // TUNING: exclude developer workstation host groups; keep production and GPU inference groups in scope. #event_simpleName=DnsRequest | DomainName=/^(hf-mirror\.com|www\.modelscope\.cn)$/i | groupBy([aid, ComputerName, DomainName, ContextBaseFileName], function=[count(as=Lookups), max(@timestamp, as=LastSeen)], limit=max) | sort(Lookups, order=desc, limit=200)
3 required fields · View requirements
Required telemetry
- DNS Resolved
Required fields
event.time: Event timeevent.type: Event typeevent.dns.request: Queried name
Recommended fields
src.process.name: Resolving process
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'DNS Resolved' event.dns.request = * | limit 5 | columns event.time, event.type, event.dns.request
// HUNT: hosts resolving the mirrors used as binary fallbacks
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1105
// CONF: low | FP: medium
// FALSE POSITIVES: sanctioned use of hf-mirror.com or ModelScope
// EQUIVALENCE: equivalent-with-schema-assumptions - Same lookup of the two model-mirror hosts, grouped per host, name and resolving process.
// REQUIRES: SentinelOne DNS Resolved events from Linux agents
// TUNING: Exclude developer workstation host groups; keep production and GPU inference groups in scope.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Hosts whose agent does not record DNS Resolved events are not covered.
// VALIDATION LEVEL: static-reviewed
event.type = 'DNS Resolved' event.dns.request in:anycase ('hf-mirror.com', 'www.modelscope.cn')
| group lookups=count(), last_seen=max(event.time) by agent.uuid, endpoint.name, event.dns.request, src.process.name
| sort -lookups
| limit 200Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceEvents
Required fields
Timestamp: Event timeActionType: Action typeAdditionalFields: Query detail
Recommended fields
InitiatingProcessFileName: Resolving process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceEvents | where Timestamp > ago(1d) | where ActionType == "DnsQueryResponse" | project Timestamp, ActionType, AdditionalFields | take 5
// HUNT: hosts resolving the mirrors used as binary fallbacks
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1105
// CONF: low | FP: medium
// FALSE POSITIVES: sanctioned use of hf-mirror.com or ModelScope
// EQUIVALENCE: equivalent-with-schema-assumptions - Same lookup of the two model-mirror hosts, read from DeviceEvents DnsQueryResponse.
// REQUIRES: Defender for Endpoint DNS telemetry on Linux sensors (DeviceEvents ActionType DnsQueryResponse)
// TUNING: Exclude developer workstation host groups; keep production and GPU inference groups in scope.
// LOOKBACK: 7d; extend to 30d for the incident window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Hosts whose sensor does not record DnsQueryResponse are not covered.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0
DeviceEvents
| where Timestamp >= ago(7d)
| where ActionType == "DnsQueryResponse"
| extend Domain = tolower(tostring(coalesce(parse_json(AdditionalFields)["DnsQueryString"], parse_json(AdditionalFields)["QueryName"])))
| where Domain in~ ("hf-mirror.com", "www.modelscope.cn")
| summarize Lookups = count(), LastSeen = max(Timestamp) by DeviceId, DeviceName, Domain, InitiatingProcessFileName
| sort by Lookups desc
| take 200Detection Rules by Platform
Three queries are specific enough to promote. All ship as detect, not block: the memfd and kworker patterns and the staging-directory names are single-source behaviours that have not been baselined against your GPU fleet, and blocking a model-serving host on an unbaselined rule interrupts production.
CrowdStrike: Custom IOA
| IOA name | Source query | Pattern | Action | Exclusions |
|---|---|---|---|---|
Load/Mine: Kernel-Worker Masquerade With Arguments | Promote Q4 (kworker arm only) | Process command line matches a bracketed kworker name followed by arguments | Detect: High | None needed for the kworker arm. Leave the memfd arm as a hunt until baselined. |
Load/Mine: Payload Directory Execution | Promote Q5 | Process command line or image path in the .cch, .cchx or .kitsd directories | Detect: High | Rare benign directory named .cch; review parent before escalation |
Load/Mine: Campaign Binary Name Or Pool Login | Promote Q3 | Image named syscore or sysx, or command line with the shared pool login | Detect: High | Check SHA256HashData against Q7 for an unrelated product named syscore |
Do not promote Q2, Q8, Q10, Q11 or Q12 to an IOA. Pool hostnames and mirrors are shared public services, port 7000 and 9001 are common application ports, and the persistence and inventory queries are low confidence by design. They are hunts.
Falcon Custom IOC ingestion
Import the CSV in Section 10 as detect only. No indicator in this pack meets the two-independent-source bar for prevent, so none is shipped as prevent. Revisit the hash rows if a second independent party (for example a sandbox verdict or another research team) publishes the same sample. Falcon IOC Management does not take wallets, pool logins or SSH keys; those are hunted with Q3.
SentinelOne: custom detection rules
| Rule | Source query | Run as | Notes |
|---|---|---|---|
| Load/Mine: Kernel-Worker Masquerade With Arguments | Q4, PowerQuery tab | Hunt only until the gap is closed | Q4 is only an approximation here. The kworker-with-arguments arm is the same predicate. The memfd arm reads the created process image path, which the sensor may record differently for a memfd or fexecve launch; the CQL itself flags this as needing tenant validation. |
| Load/Mine: Payload Directory Execution | Q5, PowerQuery tab | Scheduled custom detection rule | Bind entities so the alert names the host instead of "Unknown Device" |
| Load/Mine: Campaign Binary Name Or Pool Login | Q3, PowerQuery tab | Scheduled custom detection rule | Bind entities so the alert names the host instead of "Unknown Device" |
Create the rule with POST /web/api/v2.1/cloud-detection/rules, queryType: "scheduled" and queryLang: "2.0", with the query in data.scheduledParams.query. Use 2.0 operator spellings: a rule written with the 1.0 token ContainsCIS was accepted and never fired on a tenant. The API does not warn, so lint the body before deploying.
Microsoft Defender: custom detection rules
| Rule | Source query | Run as | Notes |
|---|---|---|---|
| Load/Mine: Kernel-Worker Masquerade With Arguments | Q4, KQL – Defender XDR tab | Hunt only until the gap is closed | Q4 is only an approximation here. The kworker-with-arguments arm is the same predicate. The memfd arm reads FolderPath, which the sensor may record differently for a memfd or fexecve launch; the CQL flags this as needing tenant validation. |
| Load/Mine: Payload Directory Execution | Q5, KQL – Defender XDR tab | Custom detection rule, alert only, every hour | The query returns Timestamp, ReportId and DeviceId, which Microsoft recommends for Defender for Endpoint tables so alerts carry the device group scope and the process tree. Remove the Timestamp filter and the final sort and take before saving the rule: the service applies the rule's lookback itself |
| Load/Mine: Campaign Binary Name Or Pool Login | Q3, KQL – Defender XDR tab | Custom detection rule, alert only, every hour | The query returns Timestamp, ReportId and DeviceId, which Microsoft recommends for Defender for Endpoint tables so alerts carry the device group scope and the process tree. Remove the Timestamp filter and the final sort and take before saving the rule: the service applies the rule's lookback itself |
Q1, Q2, Q6, Q7, Q8, Q9, Q10, Q11 and Q12 stay a hunt in this pack, not a rule. A rule can generate only 150 alerts per run. Continuous (NRT) frequency needs a single table and no comment lines, so delete the generated comment header from the query first.
Machine-Readable IOC Appendix
type,value,action,severity,expiration,description,tags ipv4,212.32.253.3,detect,critical,2027-04-30,Reverse-shell receiver (443; 9443) and payload host (8895; 8898); qwen2-tiny-5/-6/-9; zz_ext,campaign:LoadMine ipv4,38.55.133.49,detect,high,2027-04-30,zz_ext alternate second-stage host (8901; 9001); Pluto notes the C2 is now dead,campaign:LoadMine ipv4,8.137.187.63,detect,critical,2027-04-30,Wave-1 Alibaba Cloud C2 (80; 443; 50053; 50054),campaign:LoadMine ipv4,103.97.176.121,detect,high,2027-04-30,wjsbg host: m2.bin (8090); g20_reclaim.py (8899); pool port 3361,campaign:LoadMine ipv4,178.33.123.26,detect,high,2027-04-30,wjsbg/qwen2-final pool address (3361; 3362),campaign:LoadMine ipv4,2.28.9.199,detect,high,2027-04-30,r1r13 GPU and cloud-metadata exfil endpoint (7000),campaign:LoadMine domain,hz2006.aihzcc.top,detect,high,2027-04-30,wjsbg g20_reclaim.py host,campaign:LoadMine domain,prl-us.kryptex.network,detect,medium,2027-04-30,Wave-2 mining pool (8048); shared public pool service,campaign:LoadMine domain,prl-sg.kryptex.network,detect,medium,2027-04-30,Wave-2 mining pool (8048); shared public pool service,campaign:LoadMine domain,pearl-hk.luckypool.io,detect,medium,2027-04-30,Wave-1 mining pool (3361; 3956); shared public pool service,campaign:LoadMine domain,pool.hashvault.pro,detect,medium,2027-04-30,Wave-1 mining pool (80; 443); shared public pool service,campaign:LoadMine domain,pearl-sg1.luckypool.io,detect,medium,2027-04-30,soumikmahato qwen2-tiny-chat pool (3360),campaign:LoadMine domain,de.pearl.herominers.com,detect,medium,2027-04-30,soumikmahato qwen2-tiny-chat pool (1200),campaign:LoadMine domain,grows-stocks-vatican-flood.trycloudflare.com,detect,medium,2027-04-30,soumikmahato beacon receiver (/beacon),campaign:LoadMine sha256,88f4b95a1c3d0a0d9638dc6e69f5bbc9ce0e1b92d069a0e404f60fa5397ee020,detect,high,2027-04-30,Wave-2 syscore.bin (UPX with PEK! markers; 40625780 bytes),campaign:LoadMine sha256,587e3c3be3c75eb6586d69b25adb4c4208c1cf7d39157946da543dd823117adc,detect,high,2027-04-30,Wave-1 syscore.bin (33.8 MB),campaign:LoadMine sha256,b0e1ae6d73d656b203514f498b59cbcf29f067edf6fbd3803a3de7d21960848d,detect,high,2027-04-30,Wave-1 sysx.bin (7.9 MB),campaign:LoadMine sha256,5dc4b927fb91442a66a2e02c1636f629d4042a03a5c1523cd841d6ece66e0beb,detect,high,2027-04-30,wjsbg/zh-assets m2.bin miner payload (35702840 bytes),campaign:LoadMine sha256,414b3cf7a288eaa592b0f03d85c7a6960dbdb855fcb684478b9def9faa95c21d,detect,high,2027-04-30,Wave-2 modeling_evil2.py loader (not an ELF; file-scan tools only),campaign:LoadMine sha256,170ef5754bea51c3b05ae4b6c15c541ffa51d972bf8fe0bd3d1c3a19946bd6ac,detect,high,2027-04-30,htt0118/qwen2-tiny modeling_evil6.py loader,campaign:LoadMine sha256,2cd9328715bbffb6b090a58de17222c830d910f5ec8198e3fefb0fca2f3e0ef0,detect,high,2027-04-30,wjsbg/qwen2-final modeling_qtz.py loader,campaign:LoadMine sha256,849ce59ffb2ae55e4131111acd8a511444475639ba18515509310414b1661964,detect,high,2027-04-30,MENXIANGYU/qa-whl-1021 e.sh container-escape script,campaign:LoadMine sha256,6b1e531c9e799a6df1472acc143c6eaec6ea4dd22f997458ba519644d125197e,detect,medium,2027-04-30,soumikmahato sysd-kit.tgz GPU miner and CUDA runtime archive,campaign:LoadMine sha256,9d62179c94faf0c8eb86704973c218c229e92c71c0ed60a5e85d4bcc69c13f40,detect,medium,2027-04-30,soumikmahato configuration_parasit.py,campaign:LoadMine sha256,6a837d3042b9b8b6266c75b07e22ea8e7a7108083efa48ff0aedba7fa24fb57c,detect,medium,2027-04-30,soumikmahato modeling_parasit.py,campaign:LoadMine # Every row is detect. No prevent rows: every hash and address traces to Pluto Security only (the other # sources syndicate or summarise Pluto), so the two-independent-source bar for prevent is not met. # The .py and .tgz hashes are not executables and will not match in Falcon process telemetry; they are here # for file-scanning and EDR-agnostic tooling. Wallets, pool login and the SSH key are strings, not Falcon IOC types: # hunt them with Q3 and the Named Tooling block.
indicatorType,indicatorValue,action,title,description,expirationTime,severity,recommendedActions,rbacGroups,category,mitretechniques,GenerateAlert IpAddress,212.32.253.3,Audit,Reverse-shell receiver,Reverse-shell receiver (443; 9443) and payload host (8895; 8898); qwen2-tiny-5/-6/-9; zz_ext,2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True IpAddress,38.55.133.49,Audit,zz_ext alternate second-stage host,zz_ext alternate second-stage host (8901; 9001); Pluto notes the C2 is now dead,2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True IpAddress,8.137.187.63,Audit,Wave-1 Alibaba Cloud C2,Wave-1 Alibaba Cloud C2 (80; 443; 50053; 50054),2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True IpAddress,103.97.176.121,Audit,wjsbg host: m2.bin,wjsbg host: m2.bin (8090); g20_reclaim.py (8899); pool port 3361,2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True IpAddress,178.33.123.26,Audit,wjsbg/qwen2-final pool address,wjsbg/qwen2-final pool address (3361; 3362),2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True IpAddress,2.28.9.199,Audit,r1r13 GPU and cloud-metadata exfil endpoint,r1r13 GPU and cloud-metadata exfil endpoint (7000),2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True DomainName,hz2006.aihzcc.top,Audit,wjsbg g20_reclaim.py host,wjsbg g20_reclaim.py host,2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True DomainName,prl-us.kryptex.network,Audit,Wave-2 mining pool,Wave-2 mining pool (8048); shared public pool service,2027-04-30T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True DomainName,prl-sg.kryptex.network,Audit,Wave-2 mining pool,Wave-2 mining pool (8048); shared public pool service,2027-04-30T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True DomainName,pearl-hk.luckypool.io,Audit,Wave-1 mining pool,Wave-1 mining pool (3361; 3956); shared public pool service,2027-04-30T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True DomainName,pool.hashvault.pro,Audit,Wave-1 mining pool,Wave-1 mining pool (80; 443); shared public pool service,2027-04-30T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True DomainName,pearl-sg1.luckypool.io,Audit,soumikmahato qwen2-tiny-chat pool,soumikmahato qwen2-tiny-chat pool (3360),2027-04-30T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True DomainName,de.pearl.herominers.com,Audit,soumikmahato qwen2-tiny-chat pool,soumikmahato qwen2-tiny-chat pool (1200),2027-04-30T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True DomainName,grows-stocks-vatican-flood.trycloudflare.com,Audit,soumikmahato beacon receiver,soumikmahato beacon receiver (/beacon),2027-04-30T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,88f4b95a1c3d0a0d9638dc6e69f5bbc9ce0e1b92d069a0e404f60fa5397ee020,Audit,Wave-2 syscore.bin,Wave-2 syscore.bin (UPX with PEK! markers; 40625780 bytes),2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True FileSha256,587e3c3be3c75eb6586d69b25adb4c4208c1cf7d39157946da543dd823117adc,Audit,Wave-1 syscore.bin,Wave-1 syscore.bin (33.8 MB),2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True FileSha256,b0e1ae6d73d656b203514f498b59cbcf29f067edf6fbd3803a3de7d21960848d,Audit,Wave-1 sysx.bin,Wave-1 sysx.bin (7.9 MB),2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True FileSha256,5dc4b927fb91442a66a2e02c1636f629d4042a03a5c1523cd841d6ece66e0beb,Audit,wjsbg/zh-assets m2.bin miner payload,wjsbg/zh-assets m2.bin miner payload (35702840 bytes),2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True FileSha256,414b3cf7a288eaa592b0f03d85c7a6960dbdb855fcb684478b9def9faa95c21d,Audit,Wave-2 modeling_evil2.py loader,Wave-2 modeling_evil2.py loader (not an ELF; file-scan tools only),2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True FileSha256,170ef5754bea51c3b05ae4b6c15c541ffa51d972bf8fe0bd3d1c3a19946bd6ac,Audit,htt0118/qwen2-tiny modeling_evil6.py loader,htt0118/qwen2-tiny modeling_evil6.py loader,2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True FileSha256,2cd9328715bbffb6b090a58de17222c830d910f5ec8198e3fefb0fca2f3e0ef0,Audit,wjsbg/qwen2-final modeling_qtz.py loader,wjsbg/qwen2-final modeling_qtz.py loader,2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True FileSha256,849ce59ffb2ae55e4131111acd8a511444475639ba18515509310414b1661964,Audit,MENXIANGYU/qa-whl-1021 e.sh container-escape script,MENXIANGYU/qa-whl-1021 e.sh container-escape script,2027-04-30T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True FileSha256,6b1e531c9e799a6df1472acc143c6eaec6ea4dd22f997458ba519644d125197e,Audit,soumikmahato sysd-kit.tgz GPU miner and CUDA runtime archive,soumikmahato sysd-kit.tgz GPU miner and CUDA runtime archive,2027-04-30T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,9d62179c94faf0c8eb86704973c218c229e92c71c0ed60a5e85d4bcc69c13f40,Audit,soumikmahato configuration_parasit.py,soumikmahato configuration_parasit.py,2027-04-30T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,6a837d3042b9b8b6266c75b07e22ea8e7a7108083efa48ff0aedba7fa24fb57c,Audit,soumikmahato modeling_parasit.py,soumikmahato modeling_parasit.py,2027-04-30T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True
Column names and allowed values are from Microsoft's documented parameter table. Check each expirationTime before importing: an indicator whose date has passed is already expired. Download the sample CSV from the Import page to confirm the column order before a bulk import. Each file takes at most 500 indicators. Learn describes the import per entity type, so if a mixed file is rejected, split it by indicatorType. An imported indicator does nothing unless the matching Advanced features are on (for example Custom network indicators and Allow or block file).
This pack does not ship a SentinelOne import block: the import format could not be verified against SentinelOne's own documentation. Export a sample from your console and the block can be added with confidence.
Repositories by account (as of 2026-10-28; proven or wave-labelled in the source)
MENXIANGYU (wave 2): qwen2-tiny, qwen2-tiny-2, -3, -4, -5, -6, -9, qwen2-mini-c4, qwen2-mini-c5, qwen2-ezv, llama-tiny-x1019, zz_ext, qa-whl-1021
wjsbg (wave 2): eagle-tiny, qwen2-tiny-zh21, qwen2-final, zh-assets
htt0118 (wave 2): qwen2-tiny
soumikmahato (wave 2, not labelled proven): qwen2-tiny-chat
r1r13 (wave 2): qwen-lora-text-v1
hhzz667 (wave 1, removed): qwen2-shell, qwen2-tiny, qwen2-mini, qwen2-nano, qwen2-legacy, qwen2-final
dsxk4mr8tq (wave 1, removed): qwen2-tiny, -gpt2, -lite, -pure, -real, -assets
wssb123e (ModelScope, wave 1, removed): qwen2-tiny, qwen2-tiny-zh
Loader modules: modeling_evil.py, modeling_evil2.py, modeling_evil6.py, modeling_qtz.py, modeling_parasit.py, configuration_parasit.py
Binaries: syscore.bin, sysx.bin, m2.bin (peakminer; UPX markers changed to PEK!)
Second stages: lf_py.py, kitsd_v12.sh, g20_reclaim.py, e.sh, sysd-kit.tgz
Pool login: krxXJ2PKQW.worker
Wallets: prl1p9s2a9gdy4n5yfuf5suhya2d2pyvaahmz3n3gzm3npyxkj79m7yyq9kgnrv
prl1pek8rh8yjck5q7596aph7ua7xnkqs8ur9mkfmz8w7xfzcdfcfx5vsx2f4lz
83sB24CNcALNWZJrWb7zEnY6ZAtneXwfKAWyedqocXrd9cgzrLDcSdEch9xpHu6ZuqUdAh8VEMA15Zy6W5xMfZfPG4whgZj
SSH key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDYjYyvcEJW5ZVgsESyZE5uxqSwN2vmd5asrWICeHxu1 qx-5090-op
Wave-1 SSH key paths: six, including the xinference user's home
Registry receipts: qwen2-tiny-<base32>, ezv-<base32>
host paths : /tmp/.cch/{svc.py,sync,o} /tmp/.cchx /tmp/.kitsd /root/.xinference/cache (wave 1)
/etc/cron.d/.sync-helper (wave 1) /tmp/.qa_pwned ~/qa_pwned /tmp/.cache-hf/g20.py
/tmp/.c/kit.tar.gz /tmp/.c/app/ /tmp/.c.pid /tmp/.c.log /tmp/.c.out
/tmp/lf_py.py /tmp/ezv_proof.txt /mnt/qa_<pid>
startup hook : line appended to /root/.bashrc /root/.profile ~/.bashrc:
( if [ -f <svc.py> ]; then nohup <python> <svc.py> >/dev/null 2>&1 & fi )
process : [kworker/u20:1] with userspace arguments (real cmdline carries the worker ID)
watchdog relaunches the miner every 120 seconds
execution : memfd_create("kworker") then fork, setsid, fexecve
network : reverse shell to 212.32.253.3:443 or :9443 with retry after every disconnect
download client ignores HTTP_PROXY and HTTPS_PROXY
gated stage 212.32.253.3:8898/lf_py.py answers 403 "static ip." to non-allow-listed sources
loader : config.json auto_map -> modeling_evil2.EvilModel; module-level _boot() with _run() fallback
model.safetensors of 42 bytes with no tensors
# Pre-load review checklist for any repository about to be used with remote code enabled [ ] Is the repository on the approved-model list? If not, stop. [ ] Open config.json: list every auto_map entry and the Python files it names. [ ] Read each named .py file. Reject on subprocess, socket, ctypes, memfd_create, exec, eval, urllib, base64 decoding or raw IP addresses. [ ] Is model.safetensors a plausible size for the model it claims to be? (Load/Mine flagship: 42 bytes.) [ ] Pin the reviewed commit: from_pretrained(..., revision="<reviewed commit hash>"). [ ] Load only inside a sandbox: non-root, no production secrets, egress limited to the internal mirror. [ ] A clean scanner verdict is not evidence of safety: antivirus and format scanners do not read what Python does.
Detection Validation Gates
| Gate | Check | Pass Criteria |
|---|---|---|
| 1 · Telemetry ready | Confirm the Linux sensors report process creation, executable writes, outbound connections and DNS lookups (CrowdStrike: ProcessRollup2, ElfFileWritten, NetworkConnectIP4, DnsRequest. SentinelOne: Process Creation, File Creation, IP Connect, DNS Resolved. Defender: DeviceProcessEvents, DeviceFileEvents, DeviceNetworkEvents, DeviceEvents) from your GPU and inference hosts specifically, not just the general Linux fleet. | All four event types present from at least one GPU host in the last 24 h |
| 1b · Event name | Run a bare search for the ElfFileWritten event over 24 h on a Linux host that installed software recently. | Returns rows. If it returns none, record the event name your tenant actually uses and fix Q6 and Q7 before relying on them. |
| 1c · memfd image path | On an isolated test host, run a harmless program from a memfd (for example with a small, reviewed test script that you author) and record the ImageFileName the sensor stores. | The recorded form is matched by Q4's second arm, or Q4 is amended and the gap is documented |
| 2 · Benign baseline | Run Q2, Q8, Q9, Q10, Q11 and Q12 over 7 to 14 days with no exclusions and count hits per host role. | Exclusion lists written from real data before any are scheduled |
| 3 · Positive control, directory | On an isolated non-production host, create a harmless executable under a directory named .cch and run it. | Q5 and Q6 fire within the normal telemetry delay |
| 4 · Positive control, masquerade | On the same host, start a harmless process whose argv[0] is a bracketed kworker name followed by an argument. | Q4 fires |
| 5 · Positive control, network | Use an internal test listener, not the campaign address, and confirm Q10's port filter fires against it from a shell; confirm Q1 against a documentation-range stand-in you control. | Q10 fires; Q1 logic confirmed by equivalent test |
| 6 · Promotion | Promote Q3, Q4 (kworker arm) and Q5 to detect-only IOAs after gates 1 to 4 pass and a 7-day baseline shows no unexplained hits. | Hits triaged; exclusions documented; no blocking action enabled |
Hardening - Tiered
The threat picks the platform: Linux GPU hosts, containers, and the model-loading workflow. Authorities: MITRE mitigations (the why), CIS Benchmarks and NIST SP 800-190 container guidance (the what), and Pluto's recommendation on reviewing and pinning model code. Controls with no formal benchmark are flagged.
- Do not enable remote code on unreviewed repositories. Review every .py file an auto_map entry names before setting
trust_remote_code, and pin the reviewed commit withrevision=. M1033 Limit Software Installation, M1045 Code Signing (review-and-pin as the equivalent). ⚠ best-practice, no formal benchmark; the Transformers security policy places this duty on the user. Verify:grep -rn "trust_remote_code" /srv/ml /opt/ml 2>/dev/nulland require a ticket for each hit. - Authenticate and restrict the Xinference API. The payload reports through the local Xinference registry, and an exposed unauthenticated API lets an operator read the receipts. Bind to loopback or an internal interface behind an authenticating proxy. M1035 Limit Access to Resource Over Network, M1032 Multi-factor Authentication. CIS Distribution Independent Linux benchmark network-exposure guidance. Verify:
ss -ltnp | grep -E ":(9997|80)"shows no public bind. - Run model loading as a non-root user with no production secrets. The payload writes to /root files first; a non-root loader limits that. M1026 Privileged Account Management, M1018 User Account Management. CIS Linux benchmark. Verify:
ps -o user= -C python3 | sort | uniq -c. - Add the audit rules in Section 13 (Step 2). They create the shell-startup, authorized_keys, cron and memfd telemetry the Falcon sensor does not provide. M1047 Audit. Verify:
auditctl -l | grep hp_loadmine.
- Default-deny egress for GPU hosts, enforced at the network, not by proxy variables. The downloader explicitly ignores HTTP_PROXY and HTTPS_PROXY, so host-level proxy settings are not a control. Allow the internal model mirror and resolver only. M1037 Filter Network Traffic, M1030 Network Segmentation. NIST SP 800-53 SC-7. Verify: from a pilot host,
curl -sS -m 5 https://example.comfails while the mirror succeeds. - Manage authorized_keys centrally. Point sshd at root-owned key files so a user-writable authorized_keys has no effect. M1022 Restrict File and Directory Permissions, M1042 Disable or Remove Feature or Program. CIS Linux benchmark SSH section. Verify:
sshd -T | grep -i authorizedkeysfile. - Harden the container runtime for inference workloads. Non-root user, all capabilities dropped, no-new-privileges, read-only root, no privileged mode, no host disk mounts. This directly removes the container-to-host path in qa-whl-1021. M1038 Execution Prevention, M1026. CIS Docker Benchmark; NIST SP 800-190. Verify:
docker inspect --format '{{.HostConfig.Privileged}}' <container>returns false. - Mount /tmp and /dev/shm noexec,nosuid on inference hosts. This stops the on-disk fallback copy and drop-directory executables. It does not stop a memfd launch, so pair it with the seccomp step. M1038. CIS Linux benchmark. Verify:
findmnt -no OPTIONS /tmp.
- Private, reviewed model registry. Mirror only approved repositories at a pinned commit and make the mirror the only reachable model source. M1033, M1051 Update Software. ⚠ best-practice, no formal benchmark.
- Sandbox untrusted model code. Run first loads in a gVisor or microVM sandbox with no secrets and restricted network. M1048 Application Isolation and Sandboxing. NIST SP 800-190.
- Restrict memfd_create where the workload allows. A seccomp profile denying memfd_create for inference containers closes the fileless launch. Several runtimes use it legitimately, so pilot carefully. M1038. ⚠ best-practice, no formal benchmark.
- Upload-time and pre-load static checks for subprocess, socket, ctypes, memfd_create and raw-IP calls in auto_map files, as Pluto suggests would have caught every payload in this campaign. ⚠ best-practice, no formal benchmark.
Deployable Playbooks
Every step carries Prerequisites, Reboot required and Rollback. Steps 3 and 4 are staged so nothing blocks until a pilot ring has run clean.
Step 1 · Static review of a local model snapshot
# Step 1: pre-load static review of a LOCAL model snapshot (read-only; fetches nothing, executes nothing)
# Prerequisites: the repository is already downloaded to a local directory; python3 and grep installed.
# Reboot required: no
# Rollback: none required, the script only reads files.
SNAP="$1" # path to the local snapshot directory
python3 - "$SNAP" <<'PY'
import json, os, re, sys
snap = sys.argv[1]
cfg = os.path.join(snap, "config.json")
files = set()
if os.path.exists(cfg):
am = json.load(open(cfg)).get("auto_map", {})
for v in am.values():
for ref in (v if isinstance(v, list) else [v]):
if ref:
files.add(ref.split(".")[0] + ".py")
for root, _, names in os.walk(snap):
for n in names:
if n.endswith(".py"):
files.add(os.path.relpath(os.path.join(root, n), snap))
pat = re.compile(r"\b(subprocess|os\.system|socket|ctypes|memfd_create|base64\.b64decode|exec|eval|urllib)\b|\b\d{1,3}(\.\d{1,3}){3}\b")
for f in sorted(files):
p = os.path.join(snap, f)
if os.path.isfile(p):
for i, line in enumerate(open(p, errors="replace"), 1):
if pat.search(line):
print("REVIEW %s:%d: %s" % (f, i, line.strip()[:120]))
PYStep 2 · auditd rules for rc files, cron, authorized_keys and memfd
# Step 2: auditd rules for the telemetry the Falcon Linux sensor lacks # Prerequisites: auditd installed and running; confirm architecture is x86_64 (b64 rules below). # Reboot required: no (augenrules --load) # Rollback: rm /etc/audit/rules.d/hp-loadmine.rules && augenrules --load cat > /etc/audit/rules.d/hp-loadmine.rules <<'EOF' -w /root/.bashrc -p wa -k hp_loadmine_rc -w /root/.profile -p wa -k hp_loadmine_rc -w /etc/cron.d -p wa -k hp_loadmine_cron -w /var/spool/cron -p wa -k hp_loadmine_cron -w /root/.ssh/authorized_keys -p wa -k hp_loadmine_ssh -a always,exit -F arch=b64 -S memfd_create -k hp_loadmine_memfd EOF augenrules --load # Verify (CQL spot-check equivalent: forward audit logs and search the keys): auditctl -l | grep hp_loadmine ausearch -k hp_loadmine_rc -i | tail -20 # Note: add per-user rc and authorized_keys paths for the accounts that run model loaders (for example the xinference user).
Step 3 · Staged egress control for GPU hosts
# Step 3: egress control for a GPU host, staged: log first, then enforce
# Prerequisites: nftables installed; know your internal resolver and model mirror addresses. Run from the console, not over a connection this ruleset could cut.
# Reboot required: no
# Rollback: nft delete table inet hp_egress
nft add table inet hp_egress
nft add chain inet hp_egress out '{ type filter hook output priority 0; policy accept; }'
nft add rule inet hp_egress out oifname "lo" accept
nft add rule inet hp_egress out ct state established,related accept
nft add rule inet hp_egress out ip daddr REPLACE_WITH_INTERNAL_RESOLVER udp dport 53 accept
nft add rule inet hp_egress out ip daddr REPLACE_WITH_INTERNAL_MODEL_MIRROR tcp dport 443 accept
nft add rule inet hp_egress out ip daddr REPLACE_WITH_ADMIN_SUBNET tcp sport 22 accept
# Stage 1 (this ruleset): everything else is logged with a counter, not dropped. Review for a week.
nft add rule inet hp_egress out log prefix \"hp-egress-would-drop \" counter
# Stage 2, only after review and a pilot ring signs off: set the chain policy to drop.
# nft chain inet hp_egress out '{ policy drop; }'
# Verify: nft list table inet hp_egressStep 4 · Hardened container run for inference
# Step 4: run a model-serving container with a reduced attack surface
# Prerequisites: the image runs as a non-root user; model cache is on a named volume; test on a staging replica first.
# Reboot required: no (container restart only)
# Rollback: restart the container from the previous run command or compose file revision.
docker run -d --name inference-hardened \
--user 10001:10001 \
--cap-drop ALL \
--security-opt no-new-privileges \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=1g \
--network REPLACE_WITH_RESTRICTED_NETWORK \
-v models-cache:/models:rw \
--gpus all \
REPLACE_WITH_REVIEWED_IMAGE@sha256:REPLACE_WITH_DIGEST
# Do NOT use --privileged or mount host block devices; that is the path the escape script needs.
# memfd_create denial needs a custom seccomp profile (--security-opt seccomp=REPLACE_WITH_PROFILE.json). Pilot it:
# some runtimes use memfd_create legitimately and will fail to start.
# Verify:
docker inspect --format 'priv={{.HostConfig.Privileged}} user={{.Config.User}} ro={{.HostConfig.ReadonlyRootfs}}' inference-hardenedStep 5 · Root-owned authorized_keys
# Step 5: root-owned authorized_keys so user-writable key files are ignored # Prerequisites: every legitimate key already copied to /etc/ssh/authorized_keys/<user> (root:root, mode 0644). Keep a second admin session open. If keys are not migrated first, you lock users out. # Reboot required: no (sshd reload) # Rollback: rm /etc/ssh/sshd_config.d/10-hp-keys.conf && sshd -t && systemctl reload sshd install -d -m 0755 -o root -g root /etc/ssh/authorized_keys cat > /etc/ssh/sshd_config.d/10-hp-keys.conf <<'EOF' AuthorizedKeysFile /etc/ssh/authorized_keys/%u EOF sshd -t && systemctl reload sshd # Verify: sshd -T | grep -i authorizedkeysfile
Step 6 · Pin reviewed model code
# Step 6: pin reviewed model code in the loading call (application change; review in code review)
# Prerequisites: the repository was reviewed with Step 1 and the commit hash recorded.
# Reboot required: no
# Rollback: revert the commit that added revision= (this returns to loading the moving branch head, which is the unsafe state).
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
"REPLACE_WITH_APPROVED_ORG/REPLACE_WITH_APPROVED_MODEL",
revision="REPLACE_WITH_REVIEWED_COMMIT_HASH",
trust_remote_code=True, # only after the code at that exact revision has been read
)
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| Isolate | Network-contain the host in Falcon (or pull its egress at the switch or security group). Do not power off: the memfd copy and process list live in memory. Identify every other host that loaded the same repositories. | SOC / IR | Containment timestamp; list of hosts that pulled the campaign's repositories |
| Isolate: by platform | CrowdStrike: network-contain the host. SentinelOne: network quarantine (Disconnect from Network; confirm the label in your console version). Microsoft Defender: Isolate device (full network isolation). Keep the host powered on for memory capture on all of them. | SOC / IR | Containment timestamp; list of hosts that pulled the campaign's repositories |
| Preserve | Before deleting anything capture: process list with full arguments, ss -tnp, a copy of any memfd-backed binary from /proc/<pid>/exe, /tmp/.cch contents, shell startup files, authorized_keys files, cron.d, the Hugging Face modules cache, and the Xinference registrations. | IR | Hashes of every collected artifact; chain of custody |
| Rotate first | From a clean machine, rotate tokens, cloud credentials, passwords and SSH keys the loading process could reach, and review their recent use. Removing the attacker's key does not invalidate credentials it may have copied. | IR / IAM | Rotation log; review of recent credential use |
| Eradicate | Stop the watchdog before the miner, because it relaunches the miner every 120 seconds. Then remove the miner, /tmp/.cch, /tmp/.cchx, /tmp/.kitsd, the shell startup lines, cron.d/.sync-helper, the qx-5090-op key and the campaign's registry receipts. Review deletions as well as additions: the payload removes rival miners and their startup entries. | IR | Before and after file listings; startup-file diffs |
| Recover | Rebuild from a trusted image if integrity cannot be established. Redeploy with Section 12 controls: authenticated Xinference, non-root loader, egress limited to the internal mirror, pinned model revisions. | Platform / ML Ops | Image digest; hardening checklist signed off |
| Hunt wider | Run Q1, Q3 to Q7 estate-wide and Q11 to find other loading hosts. Check shared notebooks and CI jobs that pull small test models. | Threat hunt | Query results; list of hosts cleared |
Detection Coverage Map
| Technique | Behavior | CQL | IOA | CrowdStrike coverage | SentinelOne | Defender XDR |
|---|---|---|---|---|---|---|
| T1195.001 Supply chain | Malicious model repository loaded | Q11 | None | Partial Inventory only; hub-side and in-source flag use are outside endpoint telemetry | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
| T1059.006 Python | Module-level code on import | Q3, Q5 | Promote Q3, Q5 | Partial Seen through what the code launches, not the import itself | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
| T1546.004 Shell startup hook | nohup line appended to rc files | Q5, Q8 | Promote Q5 | GAP The write is in-process with no endpoint event. Detected only when the hook later fires (Q5). auditd rules in Section 13 close it. | GAP: same limit as CrowdStrike | GAP: same limit as CrowdStrike |
| T1098.004 SSH authorized_keys | Key planted in six paths (wave 1) | Q8 | None | GAP In-process write, no endpoint event. auditd and Section 7 only. | GAP: same limit as CrowdStrike | GAP: same limit as CrowdStrike |
| T1053.003 Cron | cron.d/.sync-helper (wave 1) | Q5, Q8 | None | GAP File write not seen; execution seen only if it runs a matching command line | GAP: same limit as CrowdStrike | GAP: same limit as CrowdStrike |
| T1620 Reflective code loading | memfd_create plus fexecve | Q4, Q7 | Promote Q4 | Partial Thin. The syscall is not reported; the launch may carry an unusual image path (validate with gate 1c). auditd syscall rule covers it. | Partial: Q4 approximate | Partial: Q4 approximate, Q7 telemetry gap |
| T1036 Masquerading | [kworker/u20:1] with arguments | Q4 | Promote Q4 | Good Strong when the sensor records the arguments | Partial: Q4 approximate | Partial: Q4 approximate |
| T1027.002 Packing | UPX with PEK! markers | Q7 | None | Good By hash only; marker change is not observable | Good, with assumptions | Partial: Q7 telemetry gap |
| T1095 / T1105 Network | Reverse shell, payload fetch, exfil | Q1, Q10 | None | Good For known addresses; Q10 is behavioural cover for rotation | Good, with assumptions | Good, with assumptions |
| T1496 Resource hijacking | peakminer on GPUs | Q2, Q3, Q6, Q7 | Promote Q3 | Good GPU load itself is outside endpoint telemetry | Partial: Q6 approximate | Partial: Q6 approximate, Q7 telemetry gap |
| T1611 Escape to host | LVM activation, host disk mounts | Q9 | None | Partial Host sensor only; a sensorless container is blind | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
| Container-resident activity | Everything done inside the container | None | None | GAP Depends on a sensor in the container or visibility from the host | GAP | GAP |
Gate summary: see Section 11. The honest summary of this pack is that process, directory, hash, DNS and network traces are well covered, while the fileless launch, in-process persistence writes and container-resident activity are thin or absent in Falcon telemetry and need auditd and host controls.
Hunt Summary Ticket
TITLE: Load/Mine: malicious Hugging Face model repositories delivering GPU miners and backdoors
SEVERITY: High (wave 2 live at publication; reverse shells and persistence, not only mining)
SCOPE: All Linux hosts. Priority: GPU and inference hosts (Xinference, GPUStack), shared
notebooks and CI jobs that pull small test models, and any host that loaded a
model with custom code enabled since 2026-09-21.
HYPOTHESIS: A model was loaded with trust_remote_code=True from one of the campaign's
repositories. Module-level code planted a watchdog and shell-startup hook,
launched peakminer from memory as [kworker/u20:1], and in some builds opened a
reverse shell to 212.32.253.3.
QUERIES: Q1 campaign network infrastructure [hunt, high conf]
Q2 mining-pool and campaign-host DNS [hunt, FP med]
Q3 binary names, pool login, wallets in process creation [IOA, detect]
Q4 kworker masquerade and memfd execution [IOA kworker arm]
Q5 payload directories, watchdog, startup hook [IOA, detect]
Q6 ELF written to campaign paths [hunt]
Q7 known miner binary hashes [hunt]
Q8 persistence tooling spawned by a Python parent [hunt, low conf]
Q9 container-to-host escape staging [hunt, FP med]
Q10 campaign port set from shells and interpreters [hunt, FP med]
Q11 exposure inventory, trust-remote-code launches [inventory]
Q12 hosts resolving fallback model mirrors [scoping hunt]
DO FIRST: 1. Q1 and Q7 estate-wide: cheapest answer to "did anything talk to or run this?"
2. Q3, Q5, Q6 on every host that loads models with custom code.
3. Q11 to scope which hosts load remote code at all.
4. Any host with 2 or more of Q3, Q4, Q5, Q6 = confirmed. Rotate its credentials
BEFORE cleaning it, and stop the watchdog before the miner.
FINDINGS: [ ] Hosts with campaign directories or masqueraded processes
[ ] Hosts with network hits to the listed addresses
[ ] Hosts that loaded campaign repositories (Hugging Face modules cache)
[ ] Credentials reachable from affected hosts, rotation status
GAPS: Shell startup, cron and authorized_keys writes made in-process are outside
endpoint telemetry; the memfd launch may be recorded with an unusual image path;
sensorless containers and rented or ephemeral GPU hosts are invisible. Deploy the
auditd rules (playbook step 2) and review native hunts (Section 7).
ACTIONS: Contain, preserve evidence, rotate credentials, eradicate, rebuild from trusted
image, harden per Section 12. All IOC rows ship as detect; none meets the
two-source bar for prevent.
OWNER: HuntPack
VERSION: v0.1 (2026-10-31)
Changelog
References
| Tier | Source | Used For | Access Date |
|---|---|---|---|
| TIER 1 | Pluto Security: Load/Mine, a Hugging Face malware campaign returns after takedown | Primary source for every atomic IOC, the execution chain, wave timeline, counts and defensive guidance | 2026-10-31 |
| TIER 2 | BleepingComputer: Hugging Face malware campaign returns to hijack GPUs for crypto mining | Independent corroboration of scope, targets and htt0118 persistence (via fetch-tool summary) | 2026-10-31 |
| TIER 2 | HackerNoon: Pluto Security uncovers self-iterating cryptominer campaign | Cross-check of network values; earlier figures | 2026-10-31 |
| TIER 2 | FinanceWire: Pluto Security IOC field guide | Cross-check of transcribed indicators (same origin as Tier 1) | 2026-10-31 |
| TIER 3 | Artiverse: Fake AI models turned Hugging Face into a mining trap | Secondary confirmation of persistence and reverse-shell behavior | 2026-10-31 |
| FRAMEWORK | MITRE ATT&CK | Technique and mitigation mapping | 2026-10-31 |