RubyGems Ghost Dev: 42 Malicious Crypto-Themed Gems That Open a Delayed Shell at Install
Executive Summary
On 2026-10-05 the RubyGems account reqthrottle_3474, whose profile calls itself "Ghost Dev", published 42 malicious gems in about one hour. Twenty-six carry crypto-themed or generic utility names and sixteen are typosquats of real gems such as bitcoin, keccak and solana-ruby. Every gem declares a native extension, and RubyGems runs that extension's extconf.rb during gem install or bundle install. The Ruby in lib/ is a decoy.
The loader checks that it is on a real developer workstation (no CI variables, no sandbox-like hostname, user or path, uptime over 30 minutes, and developer files such as .ssh or .gem/credentials present), decodes a Base64 payload, forks twice, detaches, sleeps 20 to 40 minutes and then runs it. Eleven gems (Template A) open a reverse /bin/sh to 45.138.12.177 on TCP 8089 or 8090. Thirty-one (Template B) download wgkit.tar.gz from the same host on TCP 8092 and run wg_install.sh. OpenSourceMalware analysed that kit: a local TLS-intercepting proxy, a clipboard hijacker and a wallet grabber that swap withdrawal addresses and send seed phrases, private keys and wallet files to a hard-coded collector.
Highest-value defensive angle. Hunt the install, not the payload. The 20 to 40 minute sleep and a double fork cut the process link between gem install and the shell (Q8 rebuilds it by host), and the campaign's own gating means a sandbox or CI scan of the gems shows nothing. Name-based hunting of the 42 gems (Q1, native hunt N1) and the one hard-coded address (Q5) are the two checks that do not depend on catching the payload in the act.
Defender priority. Treat any developer workstation that installed one of the 42 gems as compromised, as SafeDep advises: the shell runs with the installing user's rights. Rotate every credential that user could read, check wallets and exchange accounts from a clean device, and block 45.138.12.177 at the network edge.
What this pack does not claim. No actor or group name is attributed: the only designators in the sources are the account name and the profile label "Ghost Dev". On the sources' own code excerpt the payload reads /proc/uptime and treats a read failure as 0.0, so on macOS (no /proc/uptime) the uptime check should fail and the payload should not run. That is an inference from the quoted code, not a statement in the reports, so macOS hits are treated as exposure of the install, not proof of compromise. The tools in the chain (ruby, gem, bundle, sh, tar, curl) are stock software and nothing here blocks them.
Source Review & Web Hunter Notes
| Tier | Source | Key finding | Carry forward |
|---|---|---|---|
| T1 | SafeDep, 42 Malicious RubyGems Open a Reverse Shell at Install Time (2026-10-05) | Primary analysis. Account, 42-gem list with versions, extconf.rb loader code, environment checks, both payload templates, C2 address and ports, dropper command, IOC table | yes. Snapshot 01-safedep.txt |
| T2 | OpenSourceMalware, Malicious Crypto Shell Packages Target RubyGems (2026-10-05) | Independent detection and analysis. "Ghost Dev" profile name, the same account and C2, the Wallet Guard kit contents, persistence, collector endpoints, wallets and SHA-256 hashes | yes. Snapshot 04-opensourcemalware.txt. The kit analysis is single-source |
| T2 | The Hacker News, ThreatsDay bulletin | One paragraph restating the account, the 42 gems, the two payload counts and the C2 address, quoting SafeDep | partial. Snapshot 03-thehackernews.txt. Not independent of SafeDep |
| T3 | Cyber Security News, Malicious RubyGems Turn Developer Machines Into Monero Miners | Reviewed and rejected. It describes a different RubyGems campaign (Monero mining, SSH spread, other accounts and mining pools). It contains no reqthrottle_3474 or 45.138.12.177 indicator | no. Snapshot 02-cybersecuritynews.txt kept for the record; nothing from it is in this pack |
Decisions
- Independence. SafeDep and OpenSourceMalware reached the account and the C2 address separately on the same day, so
45.138.12.177has two independent reporters and is the only indicator that ships at prevent. The Hacker News repeats SafeDep. The kit hashes, collector URLs, file paths and wallet addresses come from OpenSourceMalware alone and ship detect. - Source 2 is a different campaign. The brief listed it as a source for this pack. Its content is the Monero cryptojacking and SSH-worm cluster, so its wallets, mining pools, emails and accounts are not used. Mixing them in would be the related-cluster borrowing the duplicate gate exists to stop.
- Naming. Only the designators in the sources are used: the account
reqthrottle_3474and the profile name "Ghost Dev". The kit's own label "Wallet Guard" is the operator's disguise, quoted from OpenSourceMalware. No group attribution is made. - Disagreements. SafeDep reports the reverse shell on 8089 (four named gems) and 8090 (the other seven); OpenSourceMalware reports 8090 only. Hunts cover both ports. SafeDep states that what
wg_install.shdoes is not known; OpenSourceMalware describes it, so the kit-behavior queries (Q4, Q9, Q10) are medium or low confidence. - Snapshots. The four sources were saved as plain text extracted from the fetched pages, not as model summaries, so that every indicator can be traced mechanically.
Hunt Brief & Attack Chain
Hunt hypotheses (highest fidelity first)
| # | Hypothesis | MITRE | Telemetry | Expected FP | Conf |
|---|---|---|---|---|---|
| H1 | A host contacted the collector address 45.138.12.177 on any port. | T1095, T1105, T1041 | Outbound network connection (CrowdStrike: NetworkConnectIP4. SentinelOne: IP Connect. Defender: DeviceNetworkEvents) | Scanners that probe it on purpose | HIGH |
| H2 | A developer ran a gem command naming one of the 42 gems. | T1195.001, T1204.005 | Process creation with command line (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents) | Tools that name a gem without installing | HIGH |
| H3 | A shell command carried the dropper strings .w1.tgz, wgkit.tar.gz or wg_install.sh. | T1105, T1059.004, T1070.004 | Process creation with command line (as H2) | Analysts quoting the names | HIGH |
| H4 | A ruby process built the req_throttle_mini extension. | T1195.001 | Process creation with command line (as H2) | None expected | MEDIUM |
| H5 | A ruby process started a bare interactive shell or connected out on 8089, 8090 or 8092. | T1059.004, T1095, T1571 | Process creation with parent (as H2) and outbound network connection (as H1) | Ruby console helpers, apps using those ports | MEDIUM |
| H6 | The same host shows a gem install and a ruby callback (sleep-gap correlation). | T1497.003, T1195.001 | Process creation and outbound network connection, joined by host | Developers who run Ruby apps on those ports | MEDIUM |
| H7 | The Wallet Guard kit is installed: kit file names, a loopback proxy on 8899, a rogue root CA. | T1547.013, T1553.004, T1557 | Process creation with command line (as H2) and listening-port telemetry (CrowdStrike: NetworkListenIP4) | Local dev servers on 8899 | MEDIUM |
| H8 | A Python process polls the clipboard through xclip or xsel. | T1115 | Process creation with parent (as H2) | Clipboard managers | LOW |
Attack chain
| Step | Behavior | Telemetry | Hunt angle |
|---|---|---|---|
| 1 | Developer runs gem install or bundle install for a crypto-themed or typosquatted gem. | Process creation with command line | Q1, N1 |
| 2 | RubyGems runs ext/req_throttle_mini/extconf.rb from the gemspec extensions: entry. The Ruby in lib/ is a decoy. | Process creation with command line | Q2 |
| 3 | The loader exits quietly unless the environment looks like a workstation: CI variables, sandbox-like hostname, user or path, uptime under 30 minutes or no developer files all stop it. | None. The checks are inside one Ruby process | Gap by design. See detection challenge 2 |
| 4 | Base64 payload decoded; fork twice and Process.setsid; standard streams to /dev/null; sleep 1200 to 2400 seconds; eval. The parent detaches and gem install finishes. | Process creation (lineage is broken here) | Q8, N2. See detection challenge 1 |
| 5A | Template A (11 gems): TCPSocket to 45.138.12.177 on 8089 or 8090, /bin/sh wired to the socket, retry every 30 seconds. | Outbound network connection; process creation with parent | Q5, Q6, Q7 |
| 5B | Template B (31 gems): after the delay, curl to /tmp/.w1.tgz, tar into /tmp/.w1, bash wg_install.sh, then rm -rf of both. | Process creation with command line; outbound network connection | Q3, Q5 |
| 6 | Wallet Guard kit (OpenSourceMalware): copied to $HOME/.cache/.wg, root CA trusted, proxy variables added to shell profiles, two autostart entries, loopback proxy on 8899. | Process creation; listening port | Q4, Q9; file writes are a gap |
| 7 | Collection and theft: wallet extension storage, seed phrase scan, clipboard swap about every 800 ms, withdrawal-address rewrite in proxied pages, upload to the collector on TCP 8080. | Process creation with parent; outbound network connection | Q10, Q5; file reads and in-page rewrites are gaps |
The two detection challenges
Challenge 1: the sleep breaks the lineage. The shell or downloader runs 20 to 40 minutes after gem install returned, in a process that was forked twice, given its own session and re-parented once its parent exited. A rule that waits for gem to be the ancestor of sh will never fire. This pack therefore hunts the install by name (Q1, N1), the payload by what it does regardless of ancestry (Q3, Q5, Q6, Q7), and joins the two by host (Q8). A ps check for an orphaned extconf.rb process during the sleep window is in N2. Whether a forked child that never calls exec produces its own process event on the sensor is not documented in the sources; validate it on a lab host.
Challenge 2: a detonation lab shows nothing. The campaign checks for CI variables, sandbox-style hostnames and users, scratch install paths such as /tmp/, short uptime and missing developer files, so an automated scanner, a CI job or a fresh container records a clean install with no network traffic. A "clean" verdict from a sandbox, a registry scanner that detonates, or a CI dependency check is not evidence a gem is safe. Hunt by name and by endpoint behavior on real developer machines, and do not try to reproduce the payload against the live C2 to prove the detection.
Consolidated IOC Table
| Type | Value | Conf | Action | Source snapshot | Context |
|---|---|---|---|---|---|
| RubyGems account | reqthrottle_3474 (profile name "Ghost Dev") | HIGH | detect | 01, 03, 04 | 48 gems hosted, 42 malicious, published 2026-10-05 |
| IPv4 / C2 | 45.138.12.177 | HIGH | prevent | 01, 03, 04 | Reverse shell TCP 8089 and 8090; kit download TCP 8092; collector TCP 8080. Two independent reporters (SafeDep, OpenSourceMalware). The only prevent row |
| URL | http://45.138.12.177:8092/wgkit.tar.gz | HIGH | detect | 01, 04 | Second-stage kit download (Template B, 31 gems). The IP above already carries the block |
| URL | http://45.138.12.177:8080/wi/grab | MEDIUM | detect | 04 | Grabber upload endpoint decoded by OpenSourceMalware. Single source |
| URL | http://45.138.12.177:8080/w | MEDIUM | detect | 04 | Browser-script fallback URL. Single source |
| Extension path | ext/req_throttle_mini/extconf.rb | HIGH | detect | 01, 04 | Declared in the gemspec of all 42 gems; the payload file |
| Dropped files | /tmp/.w1.tgz, /tmp/.w1/, /tmp/.w1/wg_install.sh | HIGH | detect | 01 | Template B working files, deleted by the dropper after the run |
| Environment variables | WG_KIT_URL, WG_FAST | MEDIUM | hunt | 01 | Read by the Template B payload (override URL, 2 second delay). Not visible in process telemetry; for code review |
| Persistence | $HOME/.cache/.wg, $HOME/.config/autostart/wg.desktop, wg-grab.desktop | MEDIUM | detect | 04 | Kit install location and autostart entries. Single source |
| Root CA | "Wallet Guard Root CA" (O=Wallet Guard LLC), valid 2026-10-05 to 2036-10-02 | MEDIUM | detect | 04 | Self-signed root the installer trusts. Single source |
| Local listener | 127.0.0.1:8899 | MEDIUM | hunt | 04 | Interception proxy. Port can be used by other tools |
| SHA-256 | 387a778a6376b6c060516ce5c1191a61e075acfe3fb073e6e5cd78eeb96a95b3 | MEDIUM | detect | 04 | _grab.py. Single source |
| SHA-256 | 549d35217f1e884634993ba6f13dd8e82091dd9f995a9ed0cb575958e63f073d | MEDIUM | detect | 04 | inject_proxy.py. Single source |
| SHA-256 | 57a56ab47fa7f33147d4924b07e77d773515b4a06ca9ac090874209a9950f30b | MEDIUM | detect | 04 | ext/content.js. Single source |
| SHA-256 | b857fdf59db6948ea01643d935742904e9275572e63cd23331a9405483c6379c | MEDIUM | detect | 04 | wg_install.sh. Single source |
| SHA-256 | 33276fedf0632be39b4e8a646c520bfa081bb7e67dce52042da46bbbda16d440 | MEDIUM | detect | 04 | wgkit.tar.gz. Single source |
| SHA-256 | 075b6ac656aa414e2d21b9d1bc8b0fd030902202222e9d06a326fd80d7827ae6 | MEDIUM | detect | 04 | wg-ca.crt. Single source |
| Gems | 42 gem names (full list in the Section 10 gem block) | HIGH | detect | 01, 04 | 26 crypto-themed or generic names plus 16 typosquats. Block by name in the gem mirror; not a Falcon IOC type |
| Wallet addresses | BTC, ETH, TRX, SOL, TON (Section 10 wallet block) | MEDIUM | hunt | 04 | Operator wallets embedded in the injected script. For chain analytics and exchange notification, not endpoint blocking |
Of the 42 gems, 31 are Template B (kit download) and 11 are Template A (reverse shell). SafeDep names four Template A gems (req-throttle-mini, bitcoin-rpc-lite, wallet-crypto-utils, web3-eth-utils, all on port 8089) and does not enumerate the other seven, so hunts treat all 42 names the same.
Affected Surface & Telemetry Matrix
| Surface | Telemetry | Priority | Gap risk |
|---|---|---|---|
| Linux developer workstations and WSL distributions | Process creation with command line and parent (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents); outbound network connection (CrowdStrike: NetworkConnectIP4. SentinelOne: IP Connect. Defender: DeviceNetworkEvents); listening port (CrowdStrike: NetworkListenIP4) | HIGH | File writes under /tmp and $HOME, wallet-file reads and clipboard reads have no event used here (outside endpoint telemetry as built). WSL guest activity depends on sensor coverage of the distribution. |
| macOS developer workstations | Process creation with command line (as above) | MEDIUM | The install hook runs. By the quoted code the uptime check reads /proc/uptime, which macOS lacks, so the payload should be skipped. A hit proves exposure of the gem, not compromise. Validate on a macOS test host before downgrading. |
| CI runners and build containers | Process creation (as above) | LOW | The payload refuses to run when CI variables are set or the path is under /tmp or /workspace, so a runner shows an install and no callback. Still check the lockfiles: the gem reached the build. |
| Gem mirrors, lockfiles and Gemfiles | Repository and artifact-manager inventory; Gemfile.lock content | HIGH | Outside endpoint telemetry. bundle install reads names from a Gemfile, so the command line does not carry them (Q1 misses it); use N1. |
| Developer secrets and wallets in $HOME | None used here | HIGH | The gate itself looks for .ssh, .gitconfig, .npmrc, .gem/credentials and .bundle, so the hosts the payload runs on are the ones holding developer secrets. Wallet and browser-extension storage reads are a gap. |
| Network edge and DNS | Firewall and proxy logs for 45.138.12.177 on 8080, 8089, 8090, 8092 | MEDIUM | The C2 is a bare IP, so there is no domain to hunt in DNS. Raw TCP on 8089 and 8090 is not HTTP and may not appear in proxy logs. |
ATT&CK Mapping
| Tactic | Technique | Observed behavior | Query or control |
|---|---|---|---|
| Initial Access | T1195.001 Compromise Software Dependencies and Development Tools | 42 malicious gems on RubyGems, 16 of them typosquats | Q1, Q2; PB1 |
| Execution | T1204.005 User Execution: Malicious Library | Developer installs the gem; the gemspec native extension runs extconf.rb | Q1, Q2 |
| Execution | T1059.004 Unix Shell | Reverse /bin/sh (Template A); sh -c dropper and bash wg_install.sh (Template B) | Q3, Q7 |
| Defense Evasion | T1497.001 System Checks | CI variables, hostname, user, path, uptime and developer-file checks | Gap by design (challenge 2) |
| Defense Evasion | T1497.003 Time Based Evasion | Sleep 1200 to 2400 seconds before eval or download | Q8; N2 |
| Defense Evasion | T1027 Obfuscated Files or Information; T1140 Deobfuscate/Decode Files or Information | Base64 payload; hex URL decoded with a 4-byte XOR key | Q3 (post-decode behavior) |
| Defense Evasion | T1070.004 File Deletion | Dropper removes /tmp/.w1.tgz and /tmp/.w1 after running | Q3; evidence is gone, hunt the command |
| Command and Control | T1095 Non-Application Layer Protocol | Raw TCP reverse shell to 45.138.12.177 on 8089 or 8090, 30 second retry | Q5, Q6, Q7; PB2 |
| Command and Control | T1105 Ingress Tool Transfer | curl of wgkit.tar.gz from port 8092 | Q3, Q5; PB2 |
| Command and Control | T1571 Non-Standard Port | 8089, 8090, 8092 and 8080 on the same host | Q6 |
| Persistence | T1547.013 XDG Autostart Entries | wg.desktop and wg-grab.desktop in $HOME/.config/autostart (OpenSourceMalware) | Q4; PB4 |
| Persistence | T1546.004 Unix Shell Configuration Modification | Proxy variables added to .bashrc and .zshrc (OpenSourceMalware) | N4; PB4 |
| Defense Evasion | T1553.004 Install Root Certificate | Wallet Guard Root CA trusted by the installer | Q4; N4; PB4 |
| Collection | T1557 Adversary-in-the-Middle | Local HTTP/HTTPS interception proxy on 127.0.0.1:8899 for 28 named exchange hosts | Q9 |
| Collection | T1115 Clipboard Data | Clipboard read about every 800 ms and address replacement | Q10 |
| Collection | T1005 Data from Local System | Browser wallet extension storage, Exodus and Electrum files, seed phrase, xprv, WIF and keystore scan | Gap (no file-read event used) |
| Impact | T1565.002 Transmitted Data Manipulation | Withdrawal destination replaced in page forms and requests while the page still shows the original | Gap (in-browser) |
| Exfiltration | T1041 Exfiltration Over C2 Channel | XOR-obfuscated, base64 data POSTed to the collector /grab endpoint | Q5 |
Native Audit-Log Hunts
Non-CQL checks for what endpoint telemetry does not show. Replace the angle-bracket placeholders before running. These commands only read; run them as the affected developer account, then as root for system paths.
N1 Lockfile, Gemfile and installed-gem sweep
# Repositories, build caches and shared mirrors (matches the 42 names; -w treats a hyphen as a word break) grep -rEwl --include=Gemfile --include=Gemfile.lock --include='*.gemspec' -e '(req-throttle-mini|throttle-requests|rate-limit-mini|request-guard|bitcoin-rpc-lite|web3-eth-utils|wallet-crypto-utils|eth-keystore-utils|btc-wallet-tools|eth-address-utils|crypto-mnemonic-tools|wallet-backup-tool|bip39-wordlist-utils|hdkey-derive-helper|blockchain-sync-utils|tx-broadcast-utils|electrum-protocol-lite|coinmarket-utils|web3-sign-helper|crypto-key-utils|bitcoin-address-utils|ethereum-tx-helper|merkle-proof-lite|utxo-set-utils|lightning-invoice-utils|base58-check-helper|bitcion-ruby|bitcoij-ruby|bitcion|bitciin|etheremu\.rb|etherdum\.rb|tron-rb|lightinng-invoice|ligbtning-invoice|lighthing-invoice|crypti-toolbox|crylto-toolbox|cryoto-toolbox|kecack|keccka|solaan-ruby)' /srv /opt /home 2>/dev/null # Gems already installed for the current user and system gem list --local 2>/dev/null | grep -E -w '(req-throttle-mini|throttle-requests|rate-limit-mini|request-guard|bitcoin-rpc-lite|web3-eth-utils|wallet-crypto-utils|eth-keystore-utils|btc-wallet-tools|eth-address-utils|crypto-mnemonic-tools|wallet-backup-tool|bip39-wordlist-utils|hdkey-derive-helper|blockchain-sync-utils|tx-broadcast-utils|electrum-protocol-lite|coinmarket-utils|web3-sign-helper|crypto-key-utils|bitcoin-address-utils|ethereum-tx-helper|merkle-proof-lite|utxo-set-utils|lightning-invoice-utils|base58-check-helper|bitcion-ruby|bitcoij-ruby|bitcion|bitciin|etheremu\.rb|etherdum\.rb|tron-rb|lightinng-invoice|ligbtning-invoice|lighthing-invoice|crypti-toolbox|crylto-toolbox|cryoto-toolbox|kecack|keccka|solaan-ruby)' # Review each hit. A name such as 'bitcion' can sit inside a longer legitimate name only if the hyphen word break matches.
N2 Orphaned extconf.rb process during the sleep window
# The forked child keeps the extconf.rb command line and is re-parented to PID 1 (or a user systemd/launchd) ps -eo pid,ppid,etime,user,args | awk '$2==1 || $2 ~ /^[0-9]+$/' | grep -E '[e]xtconf\.rb' # A ruby extconf.rb process that is still alive 20+ minutes after install is the sleeping payload # Where it exists, list its open sockets lsof -nP -p <PID> 2>/dev/null | grep -E 'TCP|IPv4'
N3 Template A implant: established connections
ss -tnp 2>/dev/null | grep -E '45\.138\.12\.177' # Linux lsof -nP -iTCP -sTCP:ESTABLISHED 2>/dev/null | grep -E '45\.138\.12\.177' # macOS or Linux # Template A reconnects every 30 seconds, so a live implant shows SYN-SENT or ESTABLISHED to 8089 or 8090
N4 Wallet Guard kit residue (OpenSourceMalware)
ls -la ~/.cache/.wg ~/.config/autostart/wg.desktop ~/.config/autostart/wg-grab.desktop 2>/dev/null grep -nEi 'proxy.*8899|8899' ~/.bashrc ~/.zshrc ~/.profile 2>/dev/null ss -ltnp 2>/dev/null | grep ':8899' # Linux lsof -nP -iTCP:8899 -sTCP:LISTEN 2>/dev/null # macOS or Linux # Rogue root CA (Linux trust stores, then macOS keychains) grep -rl 'Wallet Guard' /etc/ssl/certs /usr/local/share/ca-certificates /etc/pki/ca-trust 2>/dev/null security find-certificate -a -c 'Wallet Guard Root CA' /Library/Keychains/System.keychain ~/Library/Keychains/login.keychain-db 2>/dev/null
N5 Install history and gem metadata
# Where a gem was installed from and when (adjust the gem directory to your Ruby) ls -ld "$(gem env gemdir)"/gems/* 2>/dev/null | grep -E -w '(req-throttle-mini|throttle-requests|rate-limit-mini|request-guard|bitcoin-rpc-lite|web3-eth-utils|wallet-crypto-utils|eth-keystore-utils|btc-wallet-tools|eth-address-utils|crypto-mnemonic-tools|wallet-backup-tool|bip39-wordlist-utils|hdkey-derive-helper|blockchain-sync-utils|tx-broadcast-utils|electrum-protocol-lite|coinmarket-utils|web3-sign-helper|crypto-key-utils|bitcoin-address-utils|ethereum-tx-helper|merkle-proof-lite|utxo-set-utils|lightning-invoice-utils|base58-check-helper|bitcion-ruby|bitcoij-ruby|bitcion|bitciin|etheremu\.rb|etherdum\.rb|tron-rb|lightinng-invoice|ligbtning-invoice|lighthing-invoice|crypti-toolbox|crylto-toolbox|cryoto-toolbox|kecack|keccka|solaan-ruby)' find "$(gem env gemdir)" -path '*/ext/req_throttle_mini/extconf.rb' 2>/dev/null # Mirror or proxy logs: search downloads of the 42 names from 2026-10-05 onward
N6 Network edge
# Firewall, flow or proxy logs: destination 45.138.12.177, TCP 8080, 8089, 8090, 8092, from 2026-10-05 # Raw TCP on 8089 and 8090 will not appear in HTTP proxy logs; use flow or firewall records
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.
Looks for: A gem, bundle, bundler or ruby process whose command line names any of the 42 gem names from the SafeDep list, as an install, add, update or even a query. The delimiter group stops bitcion matching inside bitcoin. A bundle install that reads a Gemfile does not put gem names on the command line, so this query finds explicit installs only; use native hunt N1 for lockfiles.
Known limitations and deployment notes
- PowerQuery: A name passed through a file such as a Gemfile rather than the command line is not captured.
- PowerQuery assumes: The new process name is stored without a directory, so a versioned ruby such as ruby3.2 matches the numeric suffix pattern.
- KQL – Defender XDR: A gem fetched by a process the sensor did not observe starting, or a name passed through a file such as a Gemfile rather than the command line, is not captured.
- KQL – Defender XDR assumes: FileName holds the bare image name of the created process, so a versioned ruby such as ruby3.2 matches the numeric suffix pattern.
- KQL – Defender XDR assumes: The two quote characters are matched by hex escape inside a character class so the verbatim string needs no quote doubling.
// HUNT: Ghost Dev malicious gem named on a gem or bundle command line // MITRE: T1195.001, T1204.005 // CONF: high FP: low COST: low // REQUIRES: process creation with command line (Falcon ProcessRollup2) on developer workstations // FALSE POSITIVES: a security tool, ticket search or analyst that passes a gem name on a command line without installing it. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | FileName=/^(gem|bundle|bundler|ruby)[0-9.]*$/i | CommandLine=/(^|[\s'"=,@])(req-throttle-mini|throttle-requests|rate-limit-mini|request-guard|bitcoin-rpc-lite|web3-eth-utils|wallet-crypto-utils|eth-keystore-utils|btc-wallet-tools|eth-address-utils|crypto-mnemonic-tools|wallet-backup-tool|bip39-wordlist-utils|hdkey-derive-helper|blockchain-sync-utils|tx-broadcast-utils|electrum-protocol-lite|coinmarket-utils|web3-sign-helper|crypto-key-utils|bitcoin-address-utils|ethereum-tx-helper|merkle-proof-lite|utxo-set-utils|lightning-invoice-utils|base58-check-helper|bitcion-ruby|bitcoij-ruby|bitcion|bitciin|etheremu\.rb|etherdum\.rb|tron-rb|lightinng-invoice|ligbtning-invoice|lighthing-invoice|crypti-toolbox|crylto-toolbox|cryoto-toolbox|kecack|keccka|solaan-ruby)([\s'",@:=]|-[0-9]|$)/i | table([@timestamp, aid, ComputerName, UserName, event_platform, FileName, ParentBaseFileName, CommandLine])
4 required fields · View requirements
Required telemetry
- Process Creation events
Required fields
event.type: Event typeevent.time: Event timetgt.process.name: Created process nametgt.process.cmdline: Created command line
Recommended fields
src.process.name: Creator processendpoint.name: Host
Optional fields
src.process.user: Creator user
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, tgt.process.name, tgt.process.cmdline
// HUNT: Ghost Dev malicious gem named on a gem or bundle command line // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1195.001, T1204.005 // CONF: high | FP: low // FALSE POSITIVES: a security tool, ticket search or analyst that passes a gem name on a command line without installing it. // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate on the Process Creation event: the new process is a gem, bundle, bundler or ruby and its command line names one of the 42 listed gem names at a token boundary. matches is case-insensitive by default, which reproduces the CQL /i intent. // REQUIRES: SentinelOne Process Creation events with command line on developer workstations // TUNING: Exclude hosts or users that legitimately search for these gem names without installing them, such as a security scanner. // LOOKBACK: 30d; set the console time picker or query window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A name passed through a file such as a Gemfile rather than the command line is not captured. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' tgt.process.name matches '^(gem|bundle|bundler|ruby)[0-9.]*$' tgt.process.cmdline matches '(^|[\\s\\x27\\x22=,@])(req-throttle-mini|throttle-requests|rate-limit-mini|request-guard|bitcoin-rpc-lite|web3-eth-utils|wallet-crypto-utils|eth-keystore-utils|btc-wallet-tools|eth-address-utils|crypto-mnemonic-tools|wallet-backup-tool|bip39-wordlist-utils|hdkey-derive-helper|blockchain-sync-utils|tx-broadcast-utils|electrum-protocol-lite|coinmarket-utils|web3-sign-helper|crypto-key-utils|bitcoin-address-utils|ethereum-tx-helper|merkle-proof-lite|utxo-set-utils|lightning-invoice-utils|base58-check-helper|bitcion-ruby|bitcoij-ruby|bitcion|bitciin|etheremu\\.rb|etherdum\\.rb|tron-rb|lightinng-invoice|ligbtning-invoice|lighthing-invoice|crypti-toolbox|crylto-toolbox|cryoto-toolbox|kecack|keccka|solaan-ruby)([\\s\\x27\\x22,@:=]|-[0-9]|$)' | columns event.time, endpoint.name, endpoint.os, src.process.user, src.process.name, tgt.process.name, tgt.process.cmdline | 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
Recommended fields
InitiatingProcessFileName: Initiating process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, ProcessCommandLine | take 5
// HUNT: Ghost Dev malicious gem named on a gem or bundle command line // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1195.001, T1204.005 // CONF: high | FP: low // FALSE POSITIVES: a security tool, ticket search or analyst that passes a gem name on a command line without installing it. // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a gem, bundle, bundler or ruby process whose command line names one of the 42 listed gem names at a token boundary. The regex is case-insensitive, which reproduces the CQL /i intent. // REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents) on developer workstations // TUNING: Exclude hosts or accounts that legitimately search for these gem names without installing them, such as a security scanner. // LOOKBACK: 30d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A gem fetched by a process the sensor did not observe starting, or a name passed through a file such as a Gemfile rather than the command line, is not captured. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceProcessEvents | where Timestamp >= ago(30d) | where FileName matches regex @"(?i)^(gem|bundle|bundler|ruby)[0-9.]*$" | where ProcessCommandLine matches regex @"(?i)(^|[\s\x27\x22=,@])(req-throttle-mini|throttle-requests|rate-limit-mini|request-guard|bitcoin-rpc-lite|web3-eth-utils|wallet-crypto-utils|eth-keystore-utils|btc-wallet-tools|eth-address-utils|crypto-mnemonic-tools|wallet-backup-tool|bip39-wordlist-utils|hdkey-derive-helper|blockchain-sync-utils|tx-broadcast-utils|electrum-protocol-lite|coinmarket-utils|web3-sign-helper|crypto-key-utils|bitcoin-address-utils|ethereum-tx-helper|merkle-proof-lite|utxo-set-utils|lightning-invoice-utils|base58-check-helper|bitcion-ruby|bitcoij-ruby|bitcion|bitciin|etheremu\.rb|etherdum\.rb|tron-rb|lightinng-invoice|ligbtning-invoice|lighthing-invoice|crypti-toolbox|crylto-toolbox|cryoto-toolbox|kecack|keccka|solaan-ruby)([\s\x27\x22,@:=]|-[0-9]|$)" | project Timestamp, ReportId, DeviceId, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLine | sort by Timestamp desc | take 200
Looks for: A ruby process running extconf.rb from the req_throttle_mini extension directory. SafeDep shows the same ext/req_throttle_mini/extconf.rb declaration in the gem specification, and OpenSourceMalware names it as the payload file. Whether the directory appears in the sensor's command line depends on how RubyGems invokes the build, so confidence is medium until you confirm it on a tenant host. An infected host will often show no hit here because the build step is brief; its value is as an install-time anchor.
Known limitations and deployment notes
- PowerQuery: If the build runs with the directory name only in the working directory and not on the command line, the row is not produced.
- PowerQuery assumes: The extension directory name appears in the command line of the ruby process for a gem native-extension build; validate this in the tenant.
- KQL – Defender XDR: If the build runs with the directory name only in the working directory and not on the command line, the row is not produced.
- KQL – Defender XDR assumes: The extension directory name appears in the command line of the ruby process for a gem native-extension build; validate this in the tenant.
// HUNT: Ghost Dev req_throttle_mini extconf.rb build // MITRE: T1195.001, T1059 // CONF: medium FP: low COST: low // REQUIRES: process creation with command line (Falcon ProcessRollup2); ⚠ validate in tenant that the extension directory name appears in CommandLine for gem builds // FALSE POSITIVES: none expected; the directory name is specific to this campaign. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | FileName=/^ruby[0-9.]*$/i | CommandLine=/extconf\.rb/i | CommandLine=/req_throttle_mini/i | table([@timestamp, aid, ComputerName, UserName, event_platform, FileName, ParentBaseFileName, CommandLine])
4 required fields · View requirements
Required telemetry
- Process Creation events
Required fields
event.type: Event typeevent.time: Event timetgt.process.name: Created process nametgt.process.cmdline: Created command line
Recommended fields
src.process.name: Creator processendpoint.name: Host
Optional fields
src.process.user: Creator user
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, tgt.process.name, tgt.process.cmdline
// HUNT: Ghost Dev req_throttle_mini extconf.rb build // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1195.001, T1059 // CONF: medium | FP: low // FALSE POSITIVES: none expected; the directory name is specific to this campaign. // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate on the Process Creation event: the new process is ruby and its command line holds both extconf.rb and the req_throttle_mini directory name. contains is case-insensitive, which reproduces the CQL /i intent. // REQUIRES: SentinelOne Process Creation events with command line // TUNING: None expected; the directory name is specific to this campaign. // LOOKBACK: 30d; set the console time picker or query window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: If the build runs with the directory name only in the working directory and not on the command line, the row is not produced. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' tgt.process.name matches '^ruby[0-9.]*$' tgt.process.cmdline contains 'extconf.rb' tgt.process.cmdline contains 'req_throttle_mini' | columns event.time, endpoint.name, endpoint.os, src.process.user, src.process.name, tgt.process.name, tgt.process.cmdline | 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
Recommended fields
InitiatingProcessFileName: Initiating process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, ProcessCommandLine | take 5
// HUNT: Ghost Dev req_throttle_mini extconf.rb build // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1195.001, T1059 // CONF: medium | FP: low // FALSE POSITIVES: none expected; the directory name is specific to this campaign. // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a ruby process whose command line holds both extconf.rb and the req_throttle_mini directory name. contains is case-insensitive, which reproduces the CQL /i intent. // REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents) // TUNING: None expected; the directory name is specific to this campaign. // LOOKBACK: 30d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: If the build runs with the directory name only in the working directory and not on the command line, the row is not produced. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceProcessEvents | where Timestamp >= ago(30d) | where FileName matches regex @"(?i)^ruby[0-9.]*$" | where ProcessCommandLine contains "extconf.rb" | where ProcessCommandLine contains "req_throttle_mini" | project Timestamp, ReportId, DeviceId, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLine | sort by Timestamp desc | take 200
Looks for: Any process, normally sh -c, curl, tar or bash, whose command line carries the dropper's own strings. SafeDep shows the exact command Ruby hands to the shell: curl to /tmp/.w1.tgz, tar into /tmp/.w1, bash on wg_install.sh, then rm -rf. The strings are matched by basename (.w1.tgz, wgkit.tar.gz, wg_install.sh) so the regex needs no path slash. Template B only: 31 of the 42 gems.
Known limitations and deployment notes
- PowerQuery: A dropper run from a script file that does not repeat the artifact names on a command line is not captured.
- KQL – Defender XDR: A dropper run from a script file that does not repeat the artifact names on a command line is not captured.
- KQL – Defender XDR assumes: Linux and macOS process-creation events carry the full command line in ProcessCommandLine.
// HUNT: Ghost Dev second-stage dropper command (Template B) // MITRE: T1105, T1059.004, T1070.004 // CONF: high FP: low COST: low // REQUIRES: process creation with command line (Falcon ProcessRollup2) on Linux and macOS developer workstations // FALSE POSITIVES: an analyst or tool that echoes the filenames without running the dropper. // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | CommandLine=/\.w1\.tgz|wgkit\.tar\.gz|wg_install\.sh/i | table([@timestamp, aid, ComputerName, UserName, event_platform, FileName, ParentBaseFileName, CommandLine])
3 required fields · View requirements
Required telemetry
- Process Creation events
Required fields
event.type: Event typeevent.time: Event timetgt.process.cmdline: Created command line
Recommended fields
tgt.process.name: Created process nameendpoint.name: Host
Optional 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, tgt.process.cmdline
// HUNT: Ghost Dev second-stage dropper command (Template B)
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1105, T1059.004, T1070.004
// CONF: high | FP: low
// FALSE POSITIVES: an analyst or tool that echoes the filenames without running the dropper.
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate on the Process Creation event: the new process command line names one of the three dropper artifact names. contains is case-insensitive, which reproduces the CQL /i intent.
// REQUIRES: SentinelOne Process Creation events with command line on Linux and macOS developer workstations
// TUNING: Exclude analyst or ticketing hosts that echo the filenames without running the dropper.
// LOOKBACK: 30d; set the console time picker or query window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A dropper run from a script file that does not repeat the artifact names on a command line is not captured.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation' tgt.process.cmdline contains ('.w1.tgz', 'wgkit.tar.gz', 'wg_install.sh')
| columns event.time, endpoint.name, endpoint.os, src.process.user, src.process.name, tgt.process.name, tgt.process.cmdline
| limit 200Automated validation: static-passed
2 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeProcessCommandLine: Created command line
Recommended fields
FileName: Created process nameInitiatingProcessFileName: Initiating process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, ProcessCommandLine | take 5
// HUNT: Ghost Dev second-stage dropper command (Template B) // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1105, T1059.004, T1070.004 // CONF: high | FP: low // FALSE POSITIVES: an analyst or tool that echoes the filenames without running the dropper. // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: any created process whose command line names one of the three dropper artifact names. contains is case-insensitive, which reproduces the CQL /i intent. // REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents) on Linux and macOS developer workstations // TUNING: Exclude analyst or ticketing hosts that echo the filenames without running the dropper. // LOOKBACK: 30d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A dropper run from a script file that does not repeat the artifact names on a command line is not captured. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceProcessEvents | where Timestamp >= ago(30d) | where ProcessCommandLine contains ".w1.tgz" or ProcessCommandLine contains "wgkit.tar.gz" or ProcessCommandLine contains "wg_install.sh" | project Timestamp, ReportId, DeviceId, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLine | sort by Timestamp desc | take 200
Looks for: Process command lines naming the kit's own files: inject_proxy.py, _grab.py, wg-grab.desktop, wg.desktop, the $HOME/.cache/.wg directory, wg-ca.crt or the Wallet Guard Root CA subject. These names come from the OpenSourceMalware analysis only (SafeDep did not recover wg_install.sh), so the query ships at medium confidence and the paths are matched with escaped slashes.
Known limitations and deployment notes
- PowerQuery: Kit files touched only by file operations, with no process command line naming them, are not captured.
- KQL – Defender XDR: Kit files touched only by file operations, with no process command line naming them, are not captured.
- KQL – Defender XDR assumes: Linux process-creation events carry the full command line in ProcessCommandLine.
// HUNT: Ghost Dev Wallet Guard kit files on a command line // MITRE: T1547.013, T1553.004, T1557, T1115 // CONF: medium FP: low COST: low // REQUIRES: process creation with command line (Falcon ProcessRollup2) on Linux developer workstations // FALSE POSITIVES: none expected; every name is specific to the kit. Single source (OpenSourceMalware). // LOOKBACK: 30d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | CommandLine=/inject_proxy\.py|_grab\.py|wg-grab|wg-ca\.crt|Wallet Guard Root CA|\.cache\/\.wg/i | table([@timestamp, aid, ComputerName, UserName, event_platform, FileName, ParentBaseFileName, CommandLine])
3 required fields · View requirements
Required telemetry
- Process Creation events
Required fields
event.type: Event typeevent.time: Event timetgt.process.cmdline: Created command line
Recommended fields
tgt.process.name: Created process nameendpoint.name: Host
Optional 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, tgt.process.cmdline
// HUNT: Ghost Dev Wallet Guard kit files on a command line
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1547.013, T1553.004, T1557, T1115
// CONF: medium | FP: low
// FALSE POSITIVES: none expected; every name is specific to the kit. Single source (OpenSourceMalware).
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate on the Process Creation event: the new process command line names one of the six Wallet Guard kit artifacts. contains is case-insensitive, which reproduces the CQL /i intent.
// REQUIRES: SentinelOne Process Creation events with command line on Linux developer workstations
// TUNING: None expected; every name is specific to the kit. Single source (OpenSourceMalware).
// LOOKBACK: 30d; set the console time picker or query window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Kit files touched only by file operations, with no process command line naming them, are not captured.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation' tgt.process.cmdline contains ('inject_proxy.py', '_grab.py', 'wg-grab', 'wg-ca.crt', 'Wallet Guard Root CA', '.cache/.wg')
| columns event.time, endpoint.name, endpoint.os, src.process.user, src.process.name, tgt.process.name, tgt.process.cmdline
| limit 200Automated validation: static-passed
2 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeProcessCommandLine: Created command line
Recommended fields
FileName: Created process nameInitiatingProcessFileName: Initiating process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, ProcessCommandLine | take 5
// HUNT: Ghost Dev Wallet Guard kit files on a command line // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1547.013, T1553.004, T1557, T1115 // CONF: medium | FP: low // FALSE POSITIVES: none expected; every name is specific to the kit. Single source (OpenSourceMalware). // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: any created process whose command line names one of the six Wallet Guard kit artifacts. contains is case-insensitive, which reproduces the CQL /i intent. // REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents) on Linux developer workstations // TUNING: None expected; every name is specific to the kit. Single source (OpenSourceMalware). // LOOKBACK: 30d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Kit files touched only by file operations, with no process command line naming them, are not captured. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceProcessEvents | where Timestamp >= ago(30d) | where ProcessCommandLine contains "inject_proxy.py" or ProcessCommandLine contains "_grab.py" or ProcessCommandLine contains "wg-grab" or ProcessCommandLine contains "wg-ca.crt" or ProcessCommandLine contains "Wallet Guard Root CA" or ProcessCommandLine contains ".cache/.wg" | project Timestamp, ReportId, DeviceId, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLine | sort by Timestamp desc | take 200
Looks for: Any outbound IPv4 connection to 45.138.12.177. SafeDep and OpenSourceMalware, analysing independently, both name this address: SafeDep for the reverse shell (TCP 8089 or 8090) and the kit download (TCP 8092), OpenSourceMalware for the /wi/grab and /w collector paths on TCP 8080. All ports are matched on purpose. Falcon logs connection attempts, so a blocked attempt still fires.
Known limitations and deployment notes
- PowerQuery: Retention on the tenant may be shorter than the 90d CQL window.
- KQL – Defender XDR: Advanced Hunting retention is 30 days, so connections between 30 and 90 days old are visible only on the CQL tab.
- KQL – Defender XDR assumes: DeviceNetworkEvents records the connection attempt for the process that made it.
// HUNT: Ghost Dev connection to the 45.138.12.177 C2 and collector // MITRE: T1095, T1105, T1041 // CONF: high FP: low COST: low // REQUIRES: outbound network connection telemetry (Falcon NetworkConnectIP4) // FALSE POSITIVES: a scanner or threat-intelligence tool that probes the address on purpose. // LOOKBACK: 90d (set with the console time picker) #event_simpleName=NetworkConnectIP4 | RemoteAddressIP4=45.138.12.177 | table([@timestamp, aid, ComputerName, event_platform, ContextBaseFileName, LocalAddressIP4, LocalPort, RemoteAddressIP4, RemotePort])
3 required fields · View requirements
Required telemetry
- IP Connect events
Required fields
event.type: Event typeevent.time: Event timedst.ip.address: Remote address
Recommended fields
src.process.name: Connecting processdst.port.number: Remote port
Optional fields
src.port.number: Local 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, dst.ip.address, dst.port.number
// HUNT: Ghost Dev connection to the 45.138.12.177 C2 and collector // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1095, T1105, T1041 // CONF: high | FP: low // FALSE POSITIVES: a scanner or threat-intelligence tool that probes the address on purpose. // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate on the IP Connect event: any outbound connection to the collector address on any port. // REQUIRES: SentinelOne IP Connect events // TUNING: Exclude scanner or threat-intelligence hosts that probe the address on purpose. // LOOKBACK: 90d; limited by the tenant's retention, so set the console time picker accordingly // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Retention on the tenant may be shorter than the 90d CQL window. // VALIDATION LEVEL: static-reviewed event.type = 'IP Connect' dst.ip.address = '45.138.12.177' | columns event.time, endpoint.name, src.process.name, src.ip.address, src.port.number, dst.ip.address, dst.port.number | limit 200
Automated validation: static-passed
2 required fields · View requirements
Required telemetry
- DeviceNetworkEvents
Required fields
Timestamp: Event timeRemoteIP: Remote address
Recommended fields
InitiatingProcessFileName: Initiating processRemotePort: Remote port
Optional fields
LocalPort: Local port
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceNetworkEvents | where Timestamp > ago(1d) | project Timestamp, RemoteIP, RemotePort | take 5
// HUNT: Ghost Dev connection to the 45.138.12.177 C2 and collector // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1095, T1105, T1041 // CONF: high | FP: low // FALSE POSITIVES: a scanner or threat-intelligence tool that probes the address on purpose. // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: any outbound connection to the collector address on any port. Advanced Hunting keeps 30 days, so the window is shorter than the CQL window. // REQUIRES: Defender for Endpoint network telemetry (DeviceNetworkEvents) // TUNING: Exclude scanner or threat-intelligence hosts that probe the address on purpose. // LOOKBACK: 30d (Advanced Hunting retention; the CQL tab covers 90d) // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Advanced Hunting retention is 30 days, so connections between 30 and 90 days old are visible only on the CQL tab. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceNetworkEvents | where Timestamp >= ago(30d) | where RemoteIP == "45.138.12.177" | project Timestamp, ReportId, DeviceId, DeviceName, InitiatingProcessFileName, LocalIP, LocalPort, RemoteIP, RemotePort | sort by Timestamp desc | take 200
Looks for: A ruby process connecting to a public address on TCP 8089, 8090 or 8092, the ports SafeDep reports for the reverse shell (8089 for four gems, 8090 for the other seven) and the kit download. This is the address-independent version of Q5: it still fires if the operator moves the C2 to a new IP and keeps the template. The reconnect loop retries every 30 seconds, so a live implant produces a steady count. FP: Ruby web apps and test servers that legitimately talk to a public service on 8089, 8090 or 8092.
Known limitations and deployment notes
- PowerQuery: The 100.64.0.0/10 shared range is not excluded in either language.
- PowerQuery assumes: dst.port.number is stored as a number, so the in list compares numerically.
- KQL – Defender XDR: The CQL excludes four private and special ranges; the 100.64.0.0/10 shared range is not excluded in either language.
- KQL – Defender XDR assumes: IPv6 destinations are not evaluated by the address-range tests, matching the IPv4-only CQL.
// HUNT: Ghost Dev ruby outbound on campaign ports to a public address // MITRE: T1095, T1571 // CONF: medium FP: medium COST: medium // REQUIRES: outbound network connection telemetry (Falcon NetworkConnectIP4) with the connecting image name // FALSE POSITIVES: Ruby applications that call a public service on 8089, 8090 or 8092. // TUNING: exclude approved destinations by RemoteAddressIP4 and approved application hosts by ComputerName; a developer workstation with a count that repeats every 30 seconds is the signal. // LOOKBACK: 14d (set with the console time picker) #event_simpleName=NetworkConnectIP4 | ContextBaseFileName=/^ruby[0-9.]*$/i | RemotePort=/^(8089|8090|8092)$/ | !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"]) | groupBy([aid, ComputerName, ContextBaseFileName, RemoteAddressIP4, RemotePort], function=[count(as=connections), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)], limit=1000) | sort(connections, order=desc, limit=200)
5 required fields · View requirements
Required telemetry
- IP Connect events
Required fields
event.type: Event typeevent.time: Event timesrc.process.name: Connecting processdst.port.number: Remote portdst.ip.address: Remote address
Recommended fields
endpoint.name: Host
Optional fields
agent.uuid: Stable host key
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, src.process.name, dst.ip.address, dst.port.number
// HUNT: Ghost Dev ruby outbound on campaign ports to a public address // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1095, T1571 // CONF: medium | FP: medium // FALSE POSITIVES: Ruby applications that call a public service on 8089, 8090 or 8092. // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate and aggregation on the IP Connect event: ruby connecting out on port 8089, 8090 or 8092 to an address outside the private, loopback and link-local ranges, counted per host, process, address and port. // REQUIRES: SentinelOne IP Connect events with the connecting process name // TUNING: Exclude approved destinations by dst.ip.address and approved application hosts by endpoint.name; a developer workstation whose count repeats every 30 seconds is the signal. // LOOKBACK: 14d; set the console time picker or query window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: The 100.64.0.0/10 shared range is not excluded in either language. // VALIDATION LEVEL: static-reviewed event.type = 'IP Connect' src.process.name matches '^ruby[0-9.]*$' dst.port.number in (8089, 8090, 8092) | let internal = 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') | filter internal = false | group connections = count(), first_seen = min(event.time), last_seen = max(event.time) by agent.uuid, endpoint.name, src.process.name, dst.ip.address, dst.port.number | sort -connections | limit 200
Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceNetworkEvents
Required fields
Timestamp: Event timeInitiatingProcessFileName: Initiating processRemotePort: Remote portRemoteIP: Remote address
Recommended fields
DeviceName: Host
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceNetworkEvents | where Timestamp > ago(1d) | project Timestamp, InitiatingProcessFileName, RemoteIP, RemotePort | take 5
// HUNT: Ghost Dev ruby outbound on campaign ports to a public address // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1095, T1571 // CONF: medium | FP: medium // FALSE POSITIVES: Ruby applications that call a public service on 8089, 8090 or 8092. // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate and aggregation: ruby connecting out on port 8089, 8090 or 8092 to an address outside the private, loopback and link-local ranges, counted per host, process, address and port. // REQUIRES: Defender for Endpoint network telemetry (DeviceNetworkEvents) with the initiating process name // TUNING: Exclude approved destinations by RemoteIP and approved application hosts by DeviceName; a developer workstation whose count repeats every 30 seconds is the signal. // LOOKBACK: 14d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: The CQL excludes four private and special ranges; the 100.64.0.0/10 shared range is not excluded in either language. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0 DeviceNetworkEvents | where Timestamp >= ago(14d) | where InitiatingProcessFileName matches regex @"(?i)^ruby[0-9.]*$" | where RemotePort in (8089, 8090, 8092) | where not(ipv4_is_private(RemoteIP)) and not(ipv4_is_in_range(RemoteIP, "127.0.0.0/8")) and not(ipv4_is_in_range(RemoteIP, "169.254.0.0/16")) | summarize Connections = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceId, DeviceName, InitiatingProcessFileName, RemoteIP, RemotePort | sort by Connections desc | take 200
Looks for: A ruby parent starting sh, bash or dash with no arguments. The Template A payload calls IO.popen(['/bin/sh'], 'r+') and wires the shell to a socket, so the shell has no -c string. Ordinary Ruby tooling nearly always passes -c or a script. This is the post-sleep behavior that does not depend on the parent-child link back to gem install. FP: Ruby tools that open an interactive subshell (some console and REPL helpers).
Known limitations and deployment notes
- PowerQuery: If the double fork reparents the shell away from ruby, the creator is not ruby and the row is not produced.
- PowerQuery assumes: After the double fork of the reverse shell the creator still resolves to ruby; validate this in the tenant.
- KQL – Defender XDR: If the double fork reparents the shell away from ruby, the initiating process is not ruby and the row is not produced.
- KQL – Defender XDR assumes: After the double fork of the reverse shell the initiating process still resolves to ruby; validate this in the tenant.
// HUNT: Ghost Dev ruby spawns a bare /bin/sh (Template A reverse shell) // MITRE: T1059.004, T1095 // CONF: medium FP: medium COST: low // REQUIRES: process creation with command line and parent name (Falcon ProcessRollup2); ⚠ validate in tenant that the parent resolves to ruby after the double fork // FALSE POSITIVES: Ruby console helpers that start an interactive subshell. // TUNING: exclude known console helpers by ComputerName or UserName; a hit with an established outbound connection from the same host (Q5, Q6) is the high-confidence case. // LOOKBACK: 14d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | ParentBaseFileName=/^ruby[0-9.]*$/i | FileName=/^(sh|bash|dash)$/i | CommandLine=/^(\/bin\/|\/usr\/bin\/)?(sh|bash|dash)\s*$/i | table([@timestamp, aid, ComputerName, UserName, event_platform, FileName, ParentBaseFileName, CommandLine])
5 required fields · View requirements
Required telemetry
- Process Creation events
Required fields
event.type: Event typeevent.time: Event timesrc.process.name: Creator processtgt.process.name: Created process nametgt.process.cmdline: Created command line
Recommended fields
endpoint.name: Host
Optional fields
src.process.user: Creator user
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, src.process.name, tgt.process.name, tgt.process.cmdline
// HUNT: Ghost Dev ruby spawns a bare /bin/sh (Template A reverse shell)
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1059.004, T1095
// CONF: medium | FP: medium
// FALSE POSITIVES: Ruby console helpers that start an interactive subshell.
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate on the Process Creation event: the creator src.process is ruby and the new tgt.process is sh, bash or dash with a command line that is only the shell name, optionally with a /bin/ or /usr/bin/ prefix.
// REQUIRES: SentinelOne Process Creation events with command line
// TUNING: Exclude known console helpers by endpoint.name or src.process.user; a hit with an established outbound connection from the same host (Q5, Q6) is the high-confidence case.
// LOOKBACK: 14d; set the console time picker or query window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: If the double fork reparents the shell away from ruby, the creator is not ruby and the row is not produced.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation' src.process.name matches '^ruby[0-9.]*$' tgt.process.name in:anycase ('sh', 'bash', 'dash') tgt.process.cmdline matches '^(/bin/|/usr/bin/)?(sh|bash|dash)\\s*$'
| columns event.time, endpoint.name, endpoint.os, src.process.user, src.process.name, tgt.process.name, tgt.process.cmdline
| 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
Recommended fields
DeviceName: Host
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, InitiatingProcessFileName, FileName, ProcessCommandLine | take 5
// HUNT: Ghost Dev ruby spawns a bare /bin/sh (Template A reverse shell)
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1059.004, T1095
// CONF: medium | FP: medium
// FALSE POSITIVES: Ruby console helpers that start an interactive subshell.
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a ruby parent starting sh, bash or dash with a command line that is only the shell name, optionally with a /bin/ or /usr/bin/ prefix. The creator is InitiatingProcessFileName.
// REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents) with the initiating process name
// TUNING: Exclude known console helpers by DeviceName or AccountName; a hit with an established outbound connection from the same host (Q5, Q6) is the high-confidence case.
// LOOKBACK: 14d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: If the double fork reparents the shell away from ruby, the initiating process is not ruby and the row is not produced.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(14d)
| where InitiatingProcessFileName matches regex @"(?i)^ruby[0-9.]*$"
| where FileName in~ ("sh", "bash", "dash")
| where ProcessCommandLine matches regex @"(?i)^(/bin/|/usr/bin/)?(sh|bash|dash)\s*$"
| project Timestamp, ReportId, DeviceId, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLine
| sort by Timestamp desc
| take 200Looks for: Rebuilds the link the 20 to 40 minute sleep breaks. The parent-child chain from gem install to the shell is gone by the time the payload runs (the parent exits and the twice-forked child is re-parented), so this query matches on host instead of lineage: a host that ran a gem or bundle install and, in the same window, had ruby connect to 8089, 8090 or 8092. Narrow the time picker to a 60 to 90 minute slice after a suspect install for the cleanest read. FP: A developer who installs gems and also runs a Ruby app that uses one of those ports.
Known limitations and deployment notes
- PowerQuery: The callback half does not exclude private destinations, matching the CQL.
- PowerQuery assumes: dst.port.number is stored as a number, so the in list compares numerically.
- KQL – Defender XDR: The same-host correlation of an install and a ruby callback is not expressed in Defender XDR here.
// HUNT: Ghost Dev gem install and ruby callback on one host (sleep-gap correlation) // MITRE: T1195.001, T1497.003, T1095 // CONF: medium FP: medium COST: medium // REQUIRES: process creation (Falcon ProcessRollup2) and outbound network connection (NetworkConnectIP4) from the same host; ⚠ validate in tenant that evt collects both event names // FALSE POSITIVES: a developer who installs gems and runs a Ruby app that uses 8089, 8090 or 8092. // TUNING: run with a 90 minute time picker slice after a suspect install; exclude approved application hosts by ComputerName. // LOOKBACK: 7d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2|NetworkConnectIP4/ | (FileName=/^(gem|bundle|bundler|ruby)[0-9.]*$/i CommandLine=/\b(gem|bundle|bundler)\b.*\binstall\b/i) or (ContextBaseFileName=/^ruby[0-9.]*$/i RemotePort=/^(8089|8090|8092)$/) | evt := #event_simpleName | groupBy([aid, ComputerName], function=[count(as=events), collect([evt]), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)], limit=1000) | evt=/NetworkConnectIP4/ | evt=/ProcessRollup2/ | sort(last_seen, order=desc, limit=200)
7 required fields · View requirements
Required telemetry
- Process Creation events
- IP Connect events
Required fields
event.type: Event typeevent.time: Event timetgt.process.name: Created process nametgt.process.cmdline: Created command linesrc.process.name: Connecting processdst.port.number: Remote portagent.uuid: Stable host key
Recommended fields
endpoint.name: Host
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'IP Connect' src.process.name = * dst.port.number = * | limit 5 | columns event.time, agent.uuid, tgt.process.name, tgt.process.cmdline, src.process.name, dst.port.number
// HUNT: Ghost Dev gem install and ruby callback on one host (sleep-gap correlation) // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1195.001, T1497.003, T1095 // CONF: medium | FP: medium // FALSE POSITIVES: a developer who installs gems and runs a Ruby app that uses 8089, 8090 or 8092. // EQUIVALENCE: equivalent-with-schema-assumptions - Same host-level correlation: one host shows both a gem or bundle install process and a ruby callback on port 8089, 8090 or 8092. Both event types are selected together and counted per host, and only hosts with both are kept. // REQUIRES: SentinelOne Process Creation and IP Connect events from the same host // TUNING: Run with a 90 minute time window slice after a suspect install; exclude approved application hosts by endpoint.name. // LOOKBACK: 7d; set the console time picker or query window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: The callback half does not exclude private destinations, matching the CQL. // VALIDATION LEVEL: static-reviewed (event.type = 'Process Creation' tgt.process.name matches '^(gem|bundle|bundler|ruby)[0-9.]*$' tgt.process.cmdline matches '\\b(gem|bundle|bundler)\\b.*\\binstall\\b') or (event.type = 'IP Connect' src.process.name matches '^ruby[0-9.]*$' dst.port.number in (8089, 8090, 8092)) | group install_events = count(event.type = 'Process Creation'), callback_events = count(event.type = 'IP Connect'), first_seen = min(event.time), last_seen = max(event.time) by agent.uuid, endpoint.name | filter install_events > 0 and callback_events > 0 | sort -last_seen | limit 200
1 required field · View requirements
Required telemetry
- DeviceProcessEvents
- DeviceNetworkEvents
Required fields
Timestamp: Event time
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp | take 5
// HUNT: Ghost Dev gem install and ruby callback on one host (sleep-gap correlation) // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1195.001, T1497.003, T1095 // CONF: medium | FP: medium // FALSE POSITIVES: a developer who installs gems and runs a Ruby app that uses 8089, 8090 or 8092. // EQUIVALENCE: unsupported-without-redesign - The hunt correlates a gem or bundle install in process-creation telemetry with a ruby callback in network telemetry on the same host. That needs two Advanced Hunting tables in one query, and this pack ships one table per query. // REQUIRES: A join or union across DeviceProcessEvents and DeviceNetworkEvents // TUNING: Run the process-creation hunts (Q1 to Q3) and the network hunt (Q6) separately and compare the DeviceName values by hand. // LOOKBACK: 7d on the CQL tab // CONFIGURATION: Use the CQL tab for this card. // KNOWN GAPS: The same-host correlation of an install and a ruby callback is not expressed in Defender XDR here. // VALIDATION LEVEL: static-reviewed
Looks for: A process listening on loopback TCP 8899. OpenSourceMalware reports that inject_proxy.py listens on 127.0.0.1:8899 for HTTP and HTTPS CONNECT traffic and that the installer writes proxy variables to .bashrc and .zshrc. A single source, so medium confidence, and a port that other developer tools can use. FP: Local development servers, debuggers and proxies that bind 8899.
Known limitations and deployment notes
- PowerQuery: The listening process is not identified, only clients that connect to it; a listener with no client in the window is missed.
- PowerQuery assumes: Loopback connections are recorded as IP Connect events on this tenant.
- PowerQuery assumes: dst.port.number is stored as a number.
- KQL – Defender XDR: If the tenant does not record listening events, the query returns nothing, which reads as a clean estate.
- KQL – Defender XDR assumes: The ListeningConnectionCreated action type is populated on this tenant; it is attested in Microsoft-owned hunting repositories, not on the Learn table page.
// HUNT: Ghost Dev Wallet Guard loopback proxy listener // MITRE: T1557, T1090.001 // CONF: medium FP: medium COST: low // REQUIRES: listening-port telemetry (Falcon NetworkListenIP4) // FALSE POSITIVES: local development servers and debuggers that bind 8899. // TUNING: exclude known dev tools by ContextBaseFileName once baselined; a python listener on 8899 on a host that also hit Q3 or Q4 is the strong case. // LOOKBACK: 14d (set with the console time picker) #event_simpleName=NetworkListenIP4 | LocalPort=8899 | LocalAddressIP4=127.0.0.1 | table([@timestamp, aid, ComputerName, event_platform, ContextBaseFileName, LocalAddressIP4, LocalPort])
4 required fields · View requirements
Required telemetry
- IP Connect events
Required fields
event.type: Event typeevent.time: Event timedst.ip.address: Remote addressdst.port.number: Remote port
Recommended fields
src.process.name: Connecting processendpoint.name: Host
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'IP Connect' dst.ip.address = '127.0.0.1' | limit 5 | columns event.time, dst.ip.address, dst.port.number
// HUNT: Ghost Dev Wallet Guard loopback proxy listener // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1557, T1090.001 // CONF: medium | FP: medium // FALSE POSITIVES: local development servers and debuggers that bind 8899. // EQUIVALENCE: approximate-analytic - The listen event's field names are not attested, so this hunts the same behaviour through the attested IP Connect event: a process connecting to the loopback proxy on port 8899. A listener nobody connects to is not seen. // REQUIRES: SentinelOne IP Connect events including loopback connections // TUNING: Exclude known dev tools by src.process.name once baselined; a python process on a host that also hit Q3 or Q4 is the strong case. // LOOKBACK: 14d; set the console time picker or query window // CONFIGURATION: No lookup objects required. // KNOWN GAPS: The listening process is not identified, only clients that connect to it; a listener with no client in the window is missed. // VALIDATION LEVEL: static-reviewed event.type = 'IP Connect' dst.ip.address = '127.0.0.1' dst.port.number = 8899 | columns event.time, endpoint.name, src.process.name, src.ip.address, dst.ip.address, dst.port.number | limit 200
Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceNetworkEvents
Required fields
Timestamp: Event timeActionType: Action typeLocalIP: Local addressLocalPort: Local port
Recommended fields
InitiatingProcessFileName: Initiating process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceNetworkEvents | where Timestamp > ago(1d) | where ActionType == "ListeningConnectionCreated" | project Timestamp, ActionType, LocalIP, LocalPort | take 5
// HUNT: Ghost Dev Wallet Guard loopback proxy listener // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1557, T1090.001 // CONF: medium | FP: medium // FALSE POSITIVES: local development servers and debuggers that bind 8899. // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a listening socket created on 127.0.0.1 port 8899. The listening-connection action type is attested in Microsoft hunting repositories rather than on the Learn table page. // REQUIRES: Defender for Endpoint network telemetry (DeviceNetworkEvents) with listening-connection events // TUNING: Exclude known dev tools by InitiatingProcessFileName once baselined; a python listener on 8899 on a host that also hit Q3 or Q4 is the strong case. // LOOKBACK: 14d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: If the tenant does not record listening events, the query returns nothing, which reads as a clean estate. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceNetworkEvents | where Timestamp >= ago(14d) | where ActionType == "ListeningConnectionCreated" | where LocalIP == "127.0.0.1" and LocalPort == 8899 | project Timestamp, ReportId, DeviceId, DeviceName, InitiatingProcessFileName, LocalIP, LocalPort | sort by Timestamp desc | take 200
Looks for: Many xclip or xsel launches whose parent is a Python interpreter. OpenSourceMalware reports that the grabber reads the clipboard about every 800 milliseconds on Unix-like systems using xclip and xsel, which would produce thousands of launches an hour. This is a behavior hunt, not an indicator, so confidence is low. FP: Clipboard managers and desktop automation written in Python.
Known limitations and deployment notes
- PowerQuery: Clipboard access that does not spawn xclip or xsel, such as an in-process library call, is not captured.
- PowerQuery assumes: The creator process name is python or a versioned python such as python3.11.
- KQL – Defender XDR: Clipboard access that does not spawn xclip or xsel, such as an in-process library call, is not captured.
- KQL – Defender XDR assumes: Linux sensors record every xclip and xsel start as a process-creation event.
// HUNT: Ghost Dev clipboard polling pattern (Wallet Guard grabber) // MITRE: T1115 // CONF: low FP: medium COST: medium // REQUIRES: process creation with parent name (Falcon ProcessRollup2) on Linux desktop developer workstations // FALSE POSITIVES: clipboard managers and desktop automation scripts written in Python. // TUNING: exclude approved clipboard tools by ComputerName or by the ParentBaseFileName value you baseline; raise the threshold above your busiest legitimate tool. // LOOKBACK: 7d (set with the console time picker) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | FileName=/^(xclip|xsel)$/i | ParentBaseFileName=/^python[0-9.]*$/i | groupBy([aid, ComputerName, ParentBaseFileName], function=[count(as=clipboard_reads), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)], limit=1000) | test(clipboard_reads >= 500) | sort(clipboard_reads, order=desc, limit=200)
5 required fields · View requirements
Required telemetry
- Process Creation events
Required fields
event.type: Event typeevent.time: Event timetgt.process.name: Created process namesrc.process.name: Creator processagent.uuid: Stable host key
Recommended fields
endpoint.name: Host
Fallbacks
endpoint.name→agent.uuid: Changes the displayed host identifier only.
Readiness test
event.type = 'Process Creation' tgt.process.name = * | limit 5 | columns event.time, agent.uuid, src.process.name, tgt.process.name
// HUNT: Ghost Dev clipboard polling pattern (Wallet Guard grabber)
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1115
// CONF: low | FP: medium
// FALSE POSITIVES: clipboard managers and desktop automation scripts written in Python.
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate and threshold on the Process Creation event: xclip or xsel (the new process) created by a python creator, counted per host and creator, kept at 500 or more.
// REQUIRES: SentinelOne Process Creation events on Linux desktop developer workstations
// TUNING: Exclude approved clipboard tools by endpoint.name or by the src.process.name value you baseline; raise the threshold above your busiest legitimate tool.
// LOOKBACK: 7d; set the console time picker or query window
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Clipboard access that does not spawn xclip or xsel, such as an in-process library call, is not captured.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation' tgt.process.name in:anycase ('xclip', 'xsel') src.process.name matches '^python[0-9.]*$'
| group clipboard_reads = count(), first_seen = min(event.time), last_seen = max(event.time) by agent.uuid, endpoint.name, src.process.name
| filter clipboard_reads >= 500
| sort -clipboard_reads
| limit 200Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFileName: Created process nameInitiatingProcessFileName: Initiating process
Recommended fields
DeviceName: Host
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, InitiatingProcessFileName | take 5
// HUNT: Ghost Dev clipboard polling pattern (Wallet Guard grabber)
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1115
// CONF: low | FP: medium
// FALSE POSITIVES: clipboard managers and desktop automation scripts written in Python.
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate and threshold: xclip or xsel started by a python parent, counted per host and parent, kept at 500 or more.
// REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents) on Linux desktop developer workstations
// TUNING: Exclude approved clipboard tools by DeviceName or by the InitiatingProcessFileName value you baseline; raise the threshold above your busiest legitimate tool.
// LOOKBACK: 7d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Clipboard access that does not spawn xclip or xsel, such as an in-process library call, is not captured.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where FileName in~ ("xclip", "xsel")
| where InitiatingProcessFileName matches regex @"(?i)^python[0-9.]*$"
| summarize ClipboardReads = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceId, DeviceName, InitiatingProcessFileName
| where ClipboardReads >= 500
| sort by ClipboardReads desc
| take 200Detection Rules by Platform
CrowdStrike: Custom IOA
| IOA name | Platform / type | Pattern | Exclusions | Deployment |
|---|---|---|---|---|
| IOA-1 Ghost Dev dropper command. Promote Q3 | Linux and macOS, Process Creation | Command Line .*(\.w1\.tgz|wgkit\.tar\.gz|wg_install\.sh).* | None needed. The strings are specific to this campaign. | Detect. The pattern is a campaign string, not a tool, and the IOA does not block sh, curl or tar. |
| IOA-2 ruby spawns a bare shell. Promote Q7 | Linux, Process Creation | Parent Image File Name .*/ruby[0-9.]*$ and Command Line ^(/bin/|/usr/bin/)?(sh|bash|dash)\s*$ | Approved Ruby console helpers by host group. | Detect only. Behavioral, medium false-positive risk, and it matches a stock binary. |
| IOA-3 Connection to the collector. Promote Q5 | Linux and macOS, Network Connection | Remote IP 45.138.12.177, any port | Threat-intelligence scanners by host. | Detect. The same address is in the Falcon IOC CSV at prevent; the IOA adds the connecting process to the alert. |
| IOA-4 ruby callback on campaign ports. Promote Q6 | Linux, Network Connection | Image .*/ruby[0-9.]*$, remote port 8089, 8090 or 8092, remote address not private | Approved application hosts and destinations. | Detect only after a 14 day baseline. Behavioral, not an indicator. |
No IOA is proposed for ruby, gem, bundle, sh, tar or curl on their own. They are stock binaries and every rule above is scoped to a campaign string, a campaign address or a behavior, at detect.
SentinelOne: custom detection rules
| Rule | Source query | Run as | Notes |
|---|---|---|---|
| IOA-1 Ghost Dev dropper command. Promote Q3 | Q3, PowerQuery tab | Scheduled custom detection rule | Bind entities so the alert names the host instead of "Unknown Device" |
| IOA-2 ruby spawns a bare shell. Promote Q7 | Q7, PowerQuery tab | Scheduled custom detection rule | Bind entities so the alert names the host instead of "Unknown Device" |
| IOA-3 Connection to the collector. Promote Q5 | Q5, PowerQuery tab | Scheduled custom detection rule | Bind entities so the alert names the host instead of "Unknown Device" |
| IOA-4 ruby callback on campaign ports. Promote Q6 | Q6, PowerQuery tab | Scheduled custom detection rule | Bind entities so the alert names the host instead of "Unknown Device". This pack's own text mentions a baseline or pilot; follow it as written |
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 |
|---|---|---|---|
| IOA-1 Ghost Dev dropper command. Promote Q3 | 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 |
| IOA-2 ruby spawns a bare shell. Promote Q7 | Q7, 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 |
| IOA-3 Connection to the collector. Promote Q5 | 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 |
| IOA-4 ruby callback on campaign ports. Promote Q6 | Q6, KQL – Defender XDR tab | Custom detection rule, alert only, every hour | Make the final result include Timestamp, ReportId and DeviceId: Microsoft recommends them 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. This pack's own text mentions a baseline or pilot; follow it as written |
Q1, Q2, Q4, Q8, Q9 and Q10 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,45.138.12.177,prevent,critical,2027-04-12,Ghost Dev reverse shell C2 8089/8090 and kit host 8092/8080 (SafeDep and OpenSourceMalware independently),campaign:GhostDev-RubyGems url,http://45.138.12.177:8092/wgkit.tar.gz,detect,high,2027-04-12,Ghost Dev second-stage kit download (SafeDep and OpenSourceMalware),campaign:GhostDev-RubyGems url,http://45.138.12.177:8080/wi/grab,detect,medium,2027-04-12,Ghost Dev grabber upload endpoint (single source: OpenSourceMalware),campaign:GhostDev-RubyGems url,http://45.138.12.177:8080/w,detect,medium,2027-04-12,Ghost Dev browser-script fallback URL (single source: OpenSourceMalware),campaign:GhostDev-RubyGems sha256,387a778a6376b6c060516ce5c1191a61e075acfe3fb073e6e5cd78eeb96a95b3,detect,medium,2027-04-12,Ghost Dev Wallet Guard kit file _grab.py (single source: OpenSourceMalware),campaign:GhostDev-RubyGems sha256,549d35217f1e884634993ba6f13dd8e82091dd9f995a9ed0cb575958e63f073d,detect,medium,2027-04-12,Ghost Dev Wallet Guard kit file inject_proxy.py (single source: OpenSourceMalware),campaign:GhostDev-RubyGems sha256,57a56ab47fa7f33147d4924b07e77d773515b4a06ca9ac090874209a9950f30b,detect,medium,2027-04-12,Ghost Dev Wallet Guard kit file ext/content.js (single source: OpenSourceMalware),campaign:GhostDev-RubyGems sha256,b857fdf59db6948ea01643d935742904e9275572e63cd23331a9405483c6379c,detect,medium,2027-04-12,Ghost Dev Wallet Guard kit file wg_install.sh (single source: OpenSourceMalware),campaign:GhostDev-RubyGems sha256,33276fedf0632be39b4e8a646c520bfa081bb7e67dce52042da46bbbda16d440,detect,medium,2027-04-12,Ghost Dev Wallet Guard kit file wgkit.tar.gz (single source: OpenSourceMalware),campaign:GhostDev-RubyGems sha256,075b6ac656aa414e2d21b9d1bc8b0fd030902202222e9d06a326fd80d7827ae6,detect,medium,2027-04-12,Ghost Dev Wallet Guard kit file wg-ca.crt (single source: OpenSourceMalware),campaign:GhostDev-RubyGems
indicatorType,indicatorValue,action,title,description,expirationTime,severity,recommendedActions,rbacGroups,category,mitretechniques,GenerateAlert IpAddress,45.138.12.177,Block,Ghost Dev reverse shell C2 8089/8090 and kit host 8092/8080,Ghost Dev reverse shell C2 8089/8090 and kit host 8092/8080 (SafeDep and OpenSourceMalware independently),2027-04-12T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True Url,http://45.138.12.177:8092/wgkit.tar.gz,Audit,Ghost Dev second-stage kit download,Ghost Dev second-stage kit download (SafeDep and OpenSourceMalware),2027-04-12T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True Url,http://45.138.12.177:8080/wi/grab,Audit,Ghost Dev grabber upload endpoint,Ghost Dev grabber upload endpoint (single source: OpenSourceMalware),2027-04-12T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True Url,http://45.138.12.177:8080/w,Audit,Ghost Dev browser-script fallback URL,Ghost Dev browser-script fallback URL (single source: OpenSourceMalware),2027-04-12T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,387a778a6376b6c060516ce5c1191a61e075acfe3fb073e6e5cd78eeb96a95b3,Audit,Ghost Dev Wallet Guard kit file _grab.py,Ghost Dev Wallet Guard kit file _grab.py (single source: OpenSourceMalware),2027-04-12T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,549d35217f1e884634993ba6f13dd8e82091dd9f995a9ed0cb575958e63f073d,Audit,Ghost Dev Wallet Guard kit file inject_proxy.py,Ghost Dev Wallet Guard kit file inject_proxy.py (single source: OpenSourceMalware),2027-04-12T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,57a56ab47fa7f33147d4924b07e77d773515b4a06ca9ac090874209a9950f30b,Audit,Ghost Dev Wallet Guard kit file ext/content.js,Ghost Dev Wallet Guard kit file ext/content.js (single source: OpenSourceMalware),2027-04-12T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,b857fdf59db6948ea01643d935742904e9275572e63cd23331a9405483c6379c,Audit,Ghost Dev Wallet Guard kit file wg_install.sh,Ghost Dev Wallet Guard kit file wg_install.sh (single source: OpenSourceMalware),2027-04-12T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,33276fedf0632be39b4e8a646c520bfa081bb7e67dce52042da46bbbda16d440,Audit,Ghost Dev Wallet Guard kit file wgkit.tar.gz,Ghost Dev Wallet Guard kit file wgkit.tar.gz (single source: OpenSourceMalware),2027-04-12T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,075b6ac656aa414e2d21b9d1bc8b0fd030902202222e9d06a326fd80d7827ae6,Audit,Ghost Dev Wallet Guard kit file wg-ca.crt,Ghost Dev Wallet Guard kit file wg-ca.crt (single source: OpenSourceMalware),2027-04-12T00: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. Prevent rows import as Block, without remediation; network indicators cannot use BlockAndRemediate. 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.
# Ghost Dev malicious gems (publisher account: reqthrottle_3474), 42 of the account's 48 gems # Block by NAME in your gem mirror or artifact manager. Not a Falcon IOC type. req-throttle-mini 1.0.1 throttle-requests 1.0.1 rate-limit-mini 1.0.1 request-guard 1.0.1 bitcoin-rpc-lite 1.0.0 web3-eth-utils 1.0.0 wallet-crypto-utils 1.0.0 eth-keystore-utils 1.0.0 btc-wallet-tools 1.0.0 eth-address-utils 1.0.0 crypto-mnemonic-tools 1.0.0 wallet-backup-tool 1.0.0 bip39-wordlist-utils 1.0.0 hdkey-derive-helper 1.0.0 blockchain-sync-utils 1.0.0 tx-broadcast-utils 1.0.0 electrum-protocol-lite 1.0.0 coinmarket-utils 1.0.0 web3-sign-helper 1.0.0 crypto-key-utils 1.0.0 bitcoin-address-utils 1.0.0 ethereum-tx-helper 1.0.0 merkle-proof-lite 1.0.0 utxo-set-utils 1.0.0 lightning-invoice-utils 1.0.0 base58-check-helper 1.0.0 bitcion-ruby 1.0.0 bitcoij-ruby 1.0.0 bitcion 1.0.0 bitciin 1.0.0 etheremu.rb 1.0.0 etherdum.rb 1.0.0 tron-rb 1.0.0 lightinng-invoice 1.0.0 ligbtning-invoice 1.0.0 lighthing-invoice 1.0.0 crypti-toolbox 1.0.0 crylto-toolbox 1.0.0 cryoto-toolbox 1.0.0 kecack 1.0.0 keccka 1.0.0 solaan-ruby 1.0.0
# Host sweep list (SafeDep unless marked OSM = OpenSourceMalware only) ext/req_throttle_mini/extconf.rb (gemspec extensions: entry in all 42 gems) /tmp/.w1.tgz (dropper download, deleted after run) /tmp/.w1/ (extraction directory, deleted after run) /tmp/.w1/wg_install.sh (installer; OSM describes its behavior) $HOME/.cache/.wg (OSM: kit install directory) $HOME/.config/autostart/wg.desktop (OSM: launches the interception proxy) $HOME/.config/autostart/wg-grab.desktop (OSM: launches the wallet grabber) 127.0.0.1:8899 (OSM: loopback interception proxy) Trusted root: Wallet Guard Root CA, O=Wallet Guard LLC (OSM) Env vars read by the Template B payload: WG_KIT_URL, WG_FAST Env var read by the grabber: WG_C2 (OSM)
# Operator wallet addresses embedded in the injected script (OpenSourceMalware) # For blockchain analytics, exchange notification and customer-protection teams. Not importable as Falcon IOCs. BTC bc1qzlfqqw7zchyklj408jktazm6yzrlptrxlz0v7u ETH 0x97aF898dfB119215aFCBEb48bEeD7936A0E778Dd TRX TMhrUqZpvNsNoSXU9qfhzoEHt6LEDRb2hc SOL DC78HUMAE5QCY8M83yp3b83GL6zFQCScfbiVND7CVX8M TON UQBgGzIPtF2VjdkUocoOLFZ3rjZuU2xw3w0Q-O-O8g7rGyI3
# Behavioral signatures (tool-agnostic) 1. gem or bundle install of a package whose gemspec extensions: entry is ext/<name>/extconf.rb with a decoy lib/ file. 2. extconf.rb that reads CI variables, hostname, username, working directory, /proc/uptime and HOME dotfiles before doing anything. 3. extconf.rb that decodes Base64 to source, forks twice, calls Process.setsid, reopens stdin/stdout/stderr to /dev/null, sleeps 1200+rand(1200) seconds, then evals. 4. A ruby process that connects out, wires /bin/sh to the socket, and retries every 30 seconds. 5. sh -c with curl -o /tmp/.w1.tgz; mkdir -p /tmp/.w1; tar xzf; bash .../wg_install.sh; rm -rf. 6. A Python process on 127.0.0.1:8899 with a newly trusted root CA and proxy variables in .bashrc or .zshrc. 7. xclip or xsel launched about every 800 ms by a Python parent. 8. A gem install whose parent shell has exited and whose child process is still alive 20 to 40 minutes later (orphaned, PPID 1).
# Safe positive tests (lab host only, never against the live C2)
Q1 gem list --local bitcion (reads the local list; fetches nothing)
Q3 sh -c 'echo .w1.tgz' (prints the string; downloads nothing)
Q7 ruby -e 'IO.popen(["/bin/sh"], "r+") { |s| s.close_write }' (starts a shell and closes it at once)
Q9 python3 -m http.server 8899 --bind 127.0.0.1 (stop with Ctrl-C)
Q5, Q6, Q8: no safe test. Do not connect to 45.138.12.177. Validate with the Falcon IOC test workflow or a lab address you control.
One prevent row: the C2 address 45.138.12.177, which SafeDep and OpenSourceMalware both report. Every other row ships detect because it is single-sourced or lives on a legitimate path. No stock tool is blocked. Gem names are not a Falcon IOC type; enforce them in the gem mirror (PB1).
Detection Validation Gates
| Gate | Check | Pass criterion |
|---|---|---|
| Telemetry ready | Process creation with command line and parent (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents), outbound network connection (CrowdStrike: NetworkConnectIP4. SentinelOne: IP Connect. Defender: DeviceNetworkEvents) and listening-port telemetry (CrowdStrike: NetworkListenIP4) are reporting for Linux developer workstations. | Each event type returns rows for a known Linux developer host in the last 24 hours. Also confirm the sensor reports the connecting image name as ruby for a test connection. |
| Fork visibility | Confirm what the sensor records for a forked child that never calls exec, using a harmless lab script that forks, sleeps 60 seconds and exits. | Decide whether Q7 and Q8 can rely on a process event for the post-sleep process, or only on the network event. Record the result in the ticket. |
| Benign baseline | Run Q6, Q7, Q8, Q9 and Q10 over 14 days before alerting. | Record Ruby applications on 8089, 8090 or 8092, console helpers, local dev servers on 8899 and clipboard tools, then add them to the TUNING exclusions. |
| Positive tests | Run the harmless triggers in the Section 10 test block on a lab host. | Q1, Q3, Q7 and Q9 fire. Q5, Q6 and Q8 cannot be tested safely against the live C2. |
| Promotion | A query becomes an alert after two clean baseline weeks. | Only IOA-1 and IOA-3 may alert on first deployment; none moves to prevent. Document the owner and the rollback in the ticket. |
| Build gates | Structure, Falcon field, CQL syntax and IOC provenance linters run before this pack was staged. | All exit 0. |
Hardening: Tiered
- Block the 42 gem names and pin dependencies. Refuse the names in the gem mirror or artifact manager, commit
Gemfile.lock, and run CI and build hosts with a frozen lockfile so a new name cannot enter silently. MITRE M1033 Limit Software Installation (T1195.001, T1204.005). Authority: Bundler documentation forfrozenand lockfile checksums (checksums need Bundler 2.6 or newer; confirm withbundle --version). ⚠ best-practice, no formal benchmark. Verify:bundle config get frozen; N1 returns nothing. Playbook PB1. - Block 45.138.12.177 at every egress point. Falcon IOC at prevent (Section 10) plus a network or host firewall rule, because the reverse shell is raw TCP that a proxy will not see. MITRE M1037 Filter Network Traffic (T1095, T1105, T1041). Authority: MITRE M1037; no formal benchmark covers a single address. ⚠ best-practice, no formal benchmark. Verify: Q5 shows blocked attempts and no completed sessions;
nft list table inet huntpack_block. Playbook PB2. - Remove standing credentials from developer home directories. The gate looks for
.ssh,.gitconfig,.npmrc,.gem/credentialsand.bundle, and the shell runs as that user. Use passphrase-protected, agent-held keys, short-lived tokens and RubyGems trusted publishing in place of a stored API key. MITRE M1026 Privileged Account Management, M1027 Password Policies (T1552.001 as a consequence control). Authority: RubyGems.org guidance on trusted publishing and API key scopes. ⚠ best-practice, no formal benchmark. Verify:test ! -e ~/.gem/credentials && echo absent. Playbook PB3. - Keep seed phrases and exchange sessions off developer workstations. Use a hardware wallet and a separate, non-developer profile or device for exchange work; the kit scans for BIP39 phrases, xprv, WIF keys and keystore files. MITRE M1017 User Training, M1048 Application Isolation and Sandboxing (T1005, T1115). ⚠ best-practice, no formal benchmark. Verify: a file-name scan of $HOME for wallet, seed, mnemonic and keystore terms finds nothing live.
- Hold new gem versions behind a cooldown in the gem mirror. All 42 gems were published inside about an hour and none had history. A mirror policy that delays packages younger than a few days would have kept them out. MITRE M1033. ⚠ best-practice, no formal benchmark (depends on your mirror product). Verify: a test gem published today is refused by the mirror.
- Install dependencies in disposable build environments that do not mount the developer home. A container or VM with no
~/.sshor~/.gem/credentialslimits what any install-time code can reach. Note the limit: this campaign deliberately skips such environments, so this controls the blast radius of the next campaign, not this one. MITRE M1048 Application Isolation and Sandboxing, M1033. ⚠ best-practice, no formal benchmark. Verify:ls ~ /rootinside the build container shows no credential files. - Restrict developer-workstation egress to non-HTTP(S) internet ports. Deny outbound TCP 8000 to 9000 to the internet by default and allow-list what is needed. MITRE M1037 (T1095, T1571). Authority: CIS Linux benchmark host firewall section (control numbers differ per distribution and version). Verify: a connection test to a lab address you control on 8090 is refused.
- Alert on new trusted root certificates and autostart entries. The kit trusts its own root CA and writes two
.desktopautostart files. MITRE M1047 Audit (T1553.004, T1547.013). Authority: CIS Linux and CIS Apple macOS benchmarks cover certificate and login-item management; control numbers differ per version. Verify: N4 returns nothing on a clean host.
- Govern dependencies as an inventory. Keep an approved-gem list per team, record lockfile checksums, prefer gems with trusted publishing, and review any new native extension (
extensions:in the gemspec) because it runs code at install. MITRE M1033, M1047 (T1195.001). ⚠ best-practice, no formal benchmark. - Separate crypto and exchange activity from build and development machines. A dedicated device removes the target from the machines that install third-party code. MITRE M1048. ⚠ best-practice, no formal benchmark.
- Do not rely on
noexecon /tmp for this chain. Reasoned from the quoted dropper command, not tested:bash /tmp/.w1/wg_install.shruns a script by passing it to an interpreter, which a noexec mount does not stop. Keepnoexecfor other reasons, but do not count it here.
Deployable Playbooks
Each step lists Prerequisites, Reboot required and Rollback. Test on a pilot host first. Placeholders in angle brackets are replaced by you. Nothing here wraps an RMM tool.
PB1 Gem supply-chain lockdown (T1195.001, M1033)
PB1.1 Freeze dependencies and add lockfile checksums
Prerequisites: Bundler 2.6 or newer for checksums; write access to the project; a reviewed Gemfile.lock.
Reboot required: No
bundle --version bundle lock --add-checksums bundle config set --local frozen true # CI and build hosts: install from the frozen lockfile only bundle install # Verify bundle config get frozen
Rollback: bundle config unset --local frozen. A frozen bundle refuses any Gemfile change until you re-lock on a trusted machine and commit the new Gemfile.lock.
PB1.2 Sweep for the 42 names and block them at the mirror
Prerequisites: Read access to repositories and build caches; admin access to the gem mirror.
Reboot required: No
grep -rEwl --include=Gemfile --include=Gemfile.lock --include='*.gemspec' -e '(req-throttle-mini|throttle-requests|rate-limit-mini|request-guard|bitcoin-rpc-lite|web3-eth-utils|wallet-crypto-utils|eth-keystore-utils|btc-wallet-tools|eth-address-utils|crypto-mnemonic-tools|wallet-backup-tool|bip39-wordlist-utils|hdkey-derive-helper|blockchain-sync-utils|tx-broadcast-utils|electrum-protocol-lite|coinmarket-utils|web3-sign-helper|crypto-key-utils|bitcoin-address-utils|ethereum-tx-helper|merkle-proof-lite|utxo-set-utils|lightning-invoice-utils|base58-check-helper|bitcion-ruby|bitcoij-ruby|bitcion|bitciin|etheremu\.rb|etherdum\.rb|tron-rb|lightinng-invoice|ligbtning-invoice|lighthing-invoice|crypti-toolbox|crylto-toolbox|cryoto-toolbox|kecack|keccka|solaan-ruby)' /srv /opt /home 2>/dev/null # Gem mirror: add the 42 names from the Section 10 gem block to the block or exclude list in your product's own configuration.
Rollback: Remove the block-list entries. The sweep is read-only, so Rollback: none required for the grep.
PB2 Egress block for the C2 (T1095, T1105, M1037)
PB2.1 Linux host firewall rule with nftables
Prerequisites: Root; nftables installed and used on the host (if the host uses iptables or a firewall manager, add the equivalent rule there). The rule is not persistent across reboots until you save it in your firewall configuration.
Reboot required: No
sudo nft add table inet huntpack_block
sudo nft add chain inet huntpack_block out '{ type filter hook output priority 0; policy accept; }'
sudo nft add rule inet huntpack_block out ip daddr 45.138.12.177 counter drop
# Verify
sudo nft list table inet huntpack_block
Rollback: sudo nft delete table inet huntpack_block. The rule affects only traffic to that one address.
PB2.2 Perimeter and Falcon
Prerequisites: Firewall or secure web gateway admin; Falcon IOC Management access.
Reboot required: No
# Perimeter: deny outbound IPv4 45.138.12.177 on all ports (raw TCP 8089, 8090, 8092 and HTTP 8080 are all used). # Falcon: import the Section 10 CSV. The single ipv4 row is prevent/critical; the url and sha256 rows are detect. # Verify: run Q5 and confirm blocked attempts appear with no established session.
Rollback: Remove the perimeter rule and set the Falcon IOC to detect or delete it. Expiry is set to 2027-04-12 so the entry does not outlive the infrastructure.
PB3 Credential rotation after exposure (T1552.001, M1026)
PB3.1 Revoke and reissue developer secrets
Prerequisites: A clean device for the new keys; access to the RubyGems account, source host and cloud consoles. Do this only after the affected host is isolated (Section 14).
Reboot required: No
# On the affected account, record what existed before touching it ls -la ~/.ssh ~/.gem/credentials ~/.npmrc ~/.bundle ~/.gitconfig 2>/dev/null # Remove the stored RubyGems API key from this machine gem signout # New SSH key pair on the clean device ssh-keygen -t ed25519 -C '<owner>-<date>' -f ~/.ssh/id_ed25519_new # Then in each console: revoke the old RubyGems API keys, SSH public keys, tokens and cloud keys; review recent use. # From a clean device: move any wallet funds to a newly created wallet; change exchange passwords, API keys and 2FA.
Rollback: Not reversible by design. Do not restore a revoked credential. If a revoked key was needed by a service, issue a new key instead.
PB4 Kit removal after evidence capture (T1547.013, T1546.004, T1553.004)
PB4.1 Remove the Wallet Guard kit in place (only if you cannot rebuild)
Prerequisites: Host isolated; evidence copied (Section 14 phase 3); root for the trust-store step. Rebuilding the host from a known-good image is preferred over this.
Reboot required: No
pkill -f inject_proxy.py ; pkill -f _grab.py rm -f ~/.config/autostart/wg.desktop ~/.config/autostart/wg-grab.desktop rm -rf ~/.cache/.wg # Edit ~/.bashrc and ~/.zshrc by hand and remove the proxy variable lines the installer added (compare with your evidence copy) # Remove the trusted root CA: find where it was installed (N4), then sudo update-ca-certificates --fresh # Debian and Ubuntu, after deleting the file from /usr/local/share/ca-certificates # macOS: security delete-certificate -c 'Wallet Guard Root CA' <keychain> gem uninstall <malicious-gem-name> # Verify ls -la ~/.cache/.wg 2>/dev/null ; ss -ltnp 2>/dev/null | grep ':8899'
Rollback: Restore the removed files from the evidence archive if a cleanup step removed something legitimate, for example a proxy setting you added yourself. Re-add a CA only from a source you trust.
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| 1 Triage (read-only) | Run Q1, Q3, Q5 and Q8 for 30 days, then Q6, Q7 and Q9. Run N1 on repositories, mirrors and the host. Confirm the install time, the user and the Ruby version from shell history and gem metadata (N5). | SOC / IR | Query exports, N1 hits |
| 2 Isolate | Isolate the host with Falcon network containment before cleanup. Isolation matters for Template A: the shell runs as the developer and retries every 30 seconds. Pause CI runners that installed a listed gem. | IR | Containment record |
| 3 Preserve | Copy ~/.cache/.wg, ~/.config/autostart, shell profiles, the installed gem directory for the malicious gem, shell history and a process listing before removal. Hash everything. A forensic image is preferred. | IR / forensics | Hashed evidence archive |
| 4 Eradicate | Preferred: rebuild the host from a known-good image. If you must clean in place, follow PB4 after phase 3. | IR / sysadmin | Before and after file listing |
| 5 Rotate credentials | Treat every secret readable by the installing user as stolen (PB3): SSH keys, RubyGems API keys, npm tokens, cloud keys, browser sessions. Move wallet funds from a clean device and change exchange credentials. | IAM / dev leads / the individual | Revocation log |
| 6 Downstream | For each host that user's keys could reach, run Q5 and the Section 7 checks. If the user maintains gems or packages, compare published versions with source control. | IR / package owners | Per-host result table |
| 7 Recover and verify | Apply PB1 and PB2, rerun the query set for 30 days and require zero hits before returning the host to service. | SOC / platform | Clean exports, change tickets |
Rollback and reversibility. Eradication is destructive by design. Preserve evidence first (phase 3). Prerequisites for cleanup: root access, the host isolated, evidence copied. Reboot required: not for cleanup; a rebuild replaces the host.
Detection Coverage Map
| Technique | Behavior | CQL | IOA | CrowdStrike coverage | SentinelOne | Defender XDR |
|---|---|---|---|---|---|---|
| T1195.001 / T1204.005 | Install of one of the 42 gems by name | Q1, Q2 | None | PARTIAL bundle install from a Gemfile carries no names on the command line; N1 covers it | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
| T1095 / T1105 / T1041 | Connection to 45.138.12.177 on any port | Q5 | IOA-3 | GOOD bare-IP C2; a moved C2 is not covered by Q5 | Good, with assumptions | Good, with assumptions |
| T1095 / T1571 | ruby callback on 8089, 8090, 8092 | Q6 | IOA-4 | PARTIAL address-independent but port-dependent | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
| T1059.004 | ruby spawns a bare /bin/sh | Q7 | IOA-2 | PARTIAL depends on the sensor recording the forked child (Section 11 fork visibility gate) | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
| T1105 / T1059.004 / T1070.004 | Template B dropper command | Q3 | IOA-1 | GOOD 31 of 42 gems | Good, with assumptions | Good, with assumptions |
| T1497.003 | 20 to 40 minute sleep between install and payload | Q8 | None | PARTIAL host-level join only; lineage cannot be rebuilt | Partial: same limit as CrowdStrike | GAP |
| T1497.001 | CI, hostname, user, path, uptime and developer-file gating | None | None | GAP by design: a lab, sandbox or CI run shows nothing. Hunt real workstations | GAP | GAP |
| T1547.013 / T1553.004 / T1546.004 | Kit autostart, root CA, shell profile edits | Q4 | None | PARTIAL command-line only; file writes and trust-store changes have no event used here (N4) | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
| T1557 | Loopback proxy on 127.0.0.1:8899 | Q9 | None | PARTIAL single source; port reused by dev tools | Partial: Q9 approximate | Partial: same limit as CrowdStrike |
| T1115 | Clipboard polling | Q10 | None | GAP low-confidence behavior hunt only; in-browser address replacement is not visible | GAP: same limit as CrowdStrike | GAP: same limit as CrowdStrike |
| T1005 | Wallet extension, seed phrase and key file collection | None | None | GAP no file-read event used; rely on credential rotation and wallet hygiene | GAP | GAP |
| T1565.002 | Withdrawal address rewritten in the browser | None | None | GAP outside endpoint telemetry; exchange-side and wallet verification only | GAP | GAP |
Validation gates are in Section 11. The gaps above are real: no query in this pack covers them, and the first one is a property of the malware, not of the pack.
Hunt Summary Ticket
TITLE: Ghost Dev: 42 malicious RubyGems that run a delayed reverse shell or wallet-theft kit at gem install
SEVERITY: HIGH, developer-workstation supply chain; shell with the installing user's rights, wallet and credential theft
SCOPE: Developer workstations (Linux and WSL; macOS by exposure) that installed any of the 42 gems
published by reqthrottle_3474 on 2026-10-05, and every host and account that user could reach
HYPOTHESIS: A host that installed a Ghost Dev gem shows a gem or bundle command line naming it (Q1),
a connection to 45.138.12.177 (Q5), a ruby process on 8089/8090/8092 (Q6) or a bare shell child (Q7),
or the dropper strings .w1.tgz, wgkit.tar.gz, wg_install.sh (Q3).
QUERIES: Q1-Q10 (Section 8); IOA-1 to IOA-4 (Section 9)
DO FIRST: 1. Sweep Gemfiles, lockfiles, mirrors and installed gems for the 42 names (N1, Q1).
2. Run Q5 fleet-wide for 90 days and block 45.138.12.177 at the edge (PB2).
3. On any hit: isolate, preserve evidence, rotate every secret the user could read (PB3), check wallets.
4. Freeze dependencies and block the 42 names in the gem mirror (PB1).
FINDINGS: REPLACE_WITH_FINDINGS_SUMMARY_AFTER_HUNT_EXECUTION
GAPS: The 20-40 minute sleep breaks the install-to-payload process link (Q8 joins by host only); the campaign's
own CI/sandbox gating means a detonation lab shows nothing; file reads, trust-store changes, clipboard and in-browser
address rewriting are outside the endpoint telemetry used here (Section 15).
ACTIONS: Deploy IOA-1 to IOA-4 in detect mode; import the Section 10 CSV (one prevent row: the C2 IPv4);
enforce PB1 to PB3; validate fork visibility on a lab host before relying on Q7 and Q8.
OWNER: REPLACE_WITH_HUNT_OWNER
VERSION: v0.1 (Draft) 2026-10-12
Changelog
References
| Tier | Source | Used for | Access date |
|---|---|---|---|
| T1 | SafeDep, 42 Malicious RubyGems Open a Reverse Shell at Install Time | Account, gem list, loader and payload behavior, C2 address and ports, dropper command | 2026-10-12 |
| T2 | OpenSourceMalware, Malicious Crypto Shell Packages Target RubyGems | Ghost Dev profile name, Wallet Guard kit, persistence, collector, wallets, hashes | 2026-10-12 |
| T2 | The Hacker News, ThreatsDay bulletin | Secondary confirmation of the account, count and C2 | 2026-10-12 |
| T3 | Cyber Security News, Malicious RubyGems Turn Developer Machines Into Monero Miners | Reviewed and excluded: a different campaign | 2026-10-12 |
| Ref | MITRE ATT&CK T1195.001 | Technique mapping | 2026-10-12 |
| Ref | MITRE ATT&CK mitigations | M-number mapping | 2026-10-12 |
| Ref | Bundler, bundle config | PB1 frozen and lockfile settings | 2026-10-12 |
| Ref | RubyGems Guides, Trusted Publishing | Credential hardening | 2026-10-12 |
| Ref | CIS Benchmarks (Linux distributions, Apple macOS) | Host firewall, certificate and login-item controls | 2026-10-12 |
Four fetched sources were saved to RubyGems-GhostDev-CryptoStealer-Hunt-sources/ for the provenance gate (01 to 04).
Generated 2026-10-12 by HuntPack · defensive content only