PolinRider: Config-File Injection and Git History Rewrite
WindowsmacOSLinux
TTP Hunt medium PolinRiderContagious InterviewSupply Chain
A build-tool config file such as postcss.config.mjs growing an obfuscated tail, a .vscode/tasks.json that runs on folder open, and scripts that amend and force-push the last commit under its original timestamp: the PolinRider campaign.
What this hunt looks for
OpenSourceMalware documents PolinRider as a DPRK campaign, reported as merged with Contagious Interview, that plants an obfuscated JavaScript loader in configuration files of previously legitimate repositories. Its August 2026 guide counts more than 2,000 GitHub owners and 4,000 repositories, entered through a fake-interview project, a trojanised npm package such as tailwindcss-style-animate, or an infected developer's pull request.
The loader appends whitespace and its payload to files such as postcss.config.mjs or vite.config.js, and temp_auto_push.bat then sets the Windows clock back to the last commit, amends it with --no-verify, restores the clock and force-pushes with -uf. A config.bat referenced from .gitignore beside it is evidence of past compromise.
Other artefacts are a config file near 5,000 bytes where 80 to 200 is usual, markers such as rmcej%otb% and Cot%3t=shtP, a .vscode/tasks.json set to folderOpen that fetches from a Vercel subdomain, and node reading the next stage from TRON, with Aptos as fallback. The markers have already rotated once, so a clean string match is a first pass.
- Technique
- Compromise of software dependencies and development tools (MITRE ATT&CK T1195.001) delivering an obfuscated JavaScript loader (T1027, T1059.007) that resolves its next stage from TRON and Aptos transactions (T1102.001), with the change hidden by an amended, back-dated and force-pushed commit (T1070).
- Data sources
- EDR process telemetry, File-write telemetry, EDR network telemetry, Proxy logs, Source repository content
- Last updated
- 2026-10-04
Indicators
The atomic values this hunt matches on, listed separately from the queries so they can be lifted into tooling that speaks none of the query languages below. Every value here is reported by one of the sources cited at the foot of this page.
| Type | Value | Note |
|---|---|---|
| File name | temp_auto_push.bat |
Propagation script that amends the last commit under its original timestamp and force-pushes it; left behind in 101 victim repositories (OpenSourceMalware). |
| File name | config.bat |
Batch file referenced from .gitignore in compromised repositories, which OpenSourceMalware says to look for alongside temp_auto_push.bat. The name alone is generic. |
| File name | postcss.config.mjs, tailwind.config.js, eslint.config.mjs, next.config.mjs, vite.config.js, babel.config.js, App.js |
Files the loader appends itself to (OpenSourceMalware); postcss.config.mjs accounts for about 62% of the affected repositories it counted. |
| File path | .vscode/tasks.json |
A task with "runOn": "folderOpen" that pipes curl from a Vercel subdomain into bash or sh, or runs node ./public/fonts/fa-solid-400.woff2 (OpenSourceMalware). |
| File name | fa-solid-400.woff2, spellright.dict |
Fake font and dictionary files carrying the same loader (OpenSourceMalware). |
| String | rmcej%otb%, _$_1e42, global['!'] |
Signature marker, decoder function and injection marker of the original variant (OpenSourceMalware). |
| String | Cot%3t=shtP, MDy, global['_V'] |
The same three in the variant OpenSourceMalware identified in April 2026; global['_V'] carries a per-injection tag such as 8-st1. |
| Domain | 260120.vercel.app, default-configuration.vercel.app, vscode-settings-bootstrap.vercel.app, vscode-settings-config.vercel.app, vscode-bootstrapper.vercel.app, vscode-load-config.vercel.app |
Bootstrap hosts the tasks.json payloads fetch from, on the URL shape /settings/(mac|linux|win)?flag=<N> (OpenSourceMalware). |
| Wallet address (TRON) | TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP, TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG |
Primary and secondary dead drops, read through api.trongrid.io (OpenSourceMalware). |
| Transaction hash (Aptos) | 0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e, 0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3 |
Fallback dead drops, read through fullnode.mainnet.aptoslabs.com (OpenSourceMalware). |
| npm package | tailwindcss-style-animate, tailwind-mainanimation, tailwind-autoanimation, tailwind-animationbased, tailwindcss-typography-style, tailwindcss-style-modify, tailwindcss-animate-style |
Tailwind and PostCSS lookalikes OpenSourceMalware attributes to the campaign; tailwindcss-style-animate ^1.1.6 is the dependency of the ShoeVista take-home template. |
| UUID | e9b53a7c-2342-4b15-b02d-bd8b8f6a03f9 |
projectInfo.uuid in the tasks.json of the StakingGame take-home template, constant across victims (OpenSourceMalware). |
| IP | 166.88.54.158, 198.105.127.210, 23.27.202.27, 154.91.0.103, 136.0.9.8, 166.88.4.2, 23.27.120.142, 202.155.8.173, 166.88.134.82, 188.43.33.249, 23.27.13.43 |
Interim block list in OpenSourceMalware's August 2026 guide, which describes it as a snapshot rather than a permanent list. |
Logic
Every query below implements these steps, so the same hunt returns the same result in each product.
- Collect: process starts, file writes and network connections on Windows, macOS and Linux; repository files via YARA and osquery
- Match: a command line names temp_auto_push.bat, a Vercel bootstrap host or a TRON or Aptos dead-drop value, or node runs fa-solid-400.woff2
- Match: or temp_auto_push.bat or spellright.dict is written, or files land in a lookalike Tailwind or PostCSS package folder
- Match: or a connection to a Vercel bootstrap host, or to an interim block-list address (a rotating snapshot)
- Match: or git amends a commit or force-pushes with --no-verify, or node reaches trongrid.io or aptoslabs.com (both common in web3 work)
- Report: devices with the signal kinds seen, first and last time, newest first
Queries
Defender / Sentinel KQL
Written for this site, from indicators reported by OpenSourceMalwareOne row per device across process, file and network telemetry, where temp_auto_push.bat, a bootstrap host or a dead-drop value is the strongest hit.
// PolinRider: process, file and network signals, reported per device
let BootstrapHosts = dynamic(["260120.vercel.app", "default-configuration.vercel.app", "vscode-settings-bootstrap.vercel.app", "vscode-settings-config.vercel.app", "vscode-bootstrapper.vercel.app", "vscode-load-config.vercel.app"]);
let DeadDrops = dynamic(["TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP", "TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG", "0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e", "0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3"]);
union
(DeviceProcessEvents
| where Timestamp > ago(30d)
| where ProcessCommandLine contains "temp_auto_push.bat"
or (FileName in~ ("node.exe", "node") and ProcessCommandLine contains "fa-solid-400.woff2")
or ProcessCommandLine has_any (BootstrapHosts)
or ProcessCommandLine has_any (DeadDrops)
// optional: developers amend and force-push with hooks disabled as ordinary practice
or (FileName in~ ("git.exe", "git") and ProcessCommandLine contains "--no-verify" and (ProcessCommandLine contains "--amend" or (ProcessCommandLine contains "push" and ProcessCommandLine contains "-uf")))
| project Timestamp, DeviceName, Signal = "process", Detail = ProcessCommandLine),
(DeviceFileEvents
| where Timestamp > ago(30d)
| where FileName =~ "temp_auto_push.bat"
or (FileName =~ "spellright.dict" and InitiatingProcessFileName in~ ("node.exe", "node"))
or FolderPath matches regex @"(?i)[/\\]node_modules[/\\](tailwindcss-style-animate|tailwind-mainanimation|tailwind-autoanimation|tailwind-animationbased|tailwindcss-typography-style|tailwindcss-style-modify|tailwindcss-animate-style)[/\\]"
// optional: config.bat is a generic name, it matters beside temp_auto_push.bat or in a .gitignore
or FileName =~ "config.bat"
| project Timestamp, DeviceName, Signal = "file", Detail = FolderPath),
(DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any (BootstrapHosts)
or RemoteUrl has_any (DeadDrops)
// optional: web3 developers query TRON and Aptos from node as a matter of course
or (InitiatingProcessFileName in~ ("node.exe", "node") and RemoteUrl has_any ("trongrid.io", "aptoslabs.com"))
// optional: interim block list, a snapshot that rotates
or RemoteIP in ("166.88.54.158", "198.105.127.210", "23.27.202.27", "154.91.0.103", "136.0.9.8", "166.88.4.2", "23.27.120.142", "202.155.8.173", "166.88.134.82", "188.43.33.249", "23.27.13.43")
| project Timestamp, DeviceName, Signal = "network", Detail = strcat(RemoteUrl, " ", RemoteIP))
| summarize Signals = make_set(Signal), Events = count(), Examples = make_set(Detail, 5), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceName
| sort by LastSeen desc
Lower-fidelity parts (4)
- the git amend and force-push clause — developers amend and force-push with hooks disabled as ordinary practice. Remove: delete the `// optional: developers amend` line and the `or (FileName in~ ("git.exe", "git") ...` line below it.
- the config.bat file name — a generic name that matters only beside temp_auto_push.bat or in a .gitignore. Remove: delete the `// optional: config.bat` line and the `or FileName =~ "config.bat"` line below it.
- node reaching trongrid.io or aptoslabs.com — web3 developers query TRON and Aptos from node as a matter of course. Remove: delete the `// optional: web3 developers` line and the `or (InitiatingProcessFileName in~ ... "trongrid.io" ...)` line below it.
- the eleven interim block-list addresses — OpenSourceMalware calls the list a snapshot and it is not permanent. Remove: delete the `// optional: interim block list` line and the `or RemoteIP in (...)` line below it.
Splunk SPL
Written for this site, from indicators reported by OpenSourceMalwareRaw EDR events with the Endpoint fields, one row per host, labelled process, file or network.
```index=endpoint is a placeholder for EDR process, file and network events carrying the Endpoint data model fields```
index=endpoint earliest=-30d
| eval proc=lower(process_name), cmd=lower(process), fname=lower(file_name), path=replace(lower(file_path), "\\\\", "/"), web=lower(coalesce(url, query))
| eval signal=case(
match(cmd, "temp_auto_push\\.bat|260120\\.vercel\\.app|default-configuration\\.vercel\\.app|vscode-settings-bootstrap\\.vercel\\.app|vscode-settings-config\\.vercel\\.app|vscode-bootstrapper\\.vercel\\.app|vscode-load-config\\.vercel\\.app|tmfkqed7tjja5xnzjz2lep838vrzrs7map|txfxhuet9pjvu1bgvkbabres4yuc1ngzcg|0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e|0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3"), "process",
match(proc, "^node(\\.exe)?$") AND match(cmd, "fa-solid-400\\.woff2"), "process",
```optional: developers amend and force-push with hooks disabled as ordinary practice```
match(proc, "^git(\\.exe)?$") AND match(cmd, "--no-verify") AND (match(cmd, "--amend") OR (match(cmd, "push") AND match(cmd, "-uf"))), "process",
fname="temp_auto_push.bat" OR (fname="spellright.dict" AND match(proc, "^node(\\.exe)?$")) OR match(path, "/node_modules/(tailwindcss-style-animate|tailwind-mainanimation|tailwind-autoanimation|tailwind-animationbased|tailwindcss-typography-style|tailwindcss-style-modify|tailwindcss-animate-style)/"), "file",
```optional: config.bat is a generic name, it matters beside temp_auto_push.bat or in a .gitignore```
fname="config.bat", "file",
match(web, "260120\\.vercel\\.app|default-configuration\\.vercel\\.app|vscode-settings-bootstrap\\.vercel\\.app|vscode-settings-config\\.vercel\\.app|vscode-bootstrapper\\.vercel\\.app|vscode-load-config\\.vercel\\.app|tmfkqed7tjja5xnzjz2lep838vrzrs7map|txfxhuet9pjvu1bgvkbabres4yuc1ngzcg|0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e|0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3"), "network",
```optional: web3 developers query TRON and Aptos from node as a matter of course```
match(proc, "^node(\\.exe)?$") AND match(web, "trongrid\\.io|aptoslabs\\.com"), "network",
```optional: interim block list, a snapshot that rotates```
in(dest_ip, "166.88.54.158", "198.105.127.210", "23.27.202.27", "154.91.0.103", "136.0.9.8", "166.88.4.2", "23.27.120.142", "202.155.8.173", "166.88.134.82", "188.43.33.249", "23.27.13.43"), "network",
true(), null())
| where isnotnull(signal)
| eval detail=coalesce(path, process, web)
| stats count AS events values(signal) AS signals values(detail) AS examples min(_time) AS first_seen max(_time) AS last_seen BY host
| convert ctime(first_seen) ctime(last_seen)
| sort - last_seen
Lower-fidelity parts (4)
- the git amend and force-push clause — developers amend and force-push with hooks disabled as ordinary practice. Remove: delete the ```optional: developers amend``` comment and the `match(proc, "^git(\\.exe)?$") ...` line below it.
- the config.bat file name — a generic name that matters only beside temp_auto_push.bat or in a .gitignore. Remove: delete the ```optional: config.bat``` comment and the `fname="config.bat", "file",` line below it.
- node reaching trongrid.io or aptoslabs.com — web3 developers query TRON and Aptos from node as a matter of course. Remove: delete the ```optional: web3 developers``` comment and the `match(proc, "^node(\\.exe)?$") AND match(web, "trongrid...` line below it.
- the eleven interim block-list addresses — OpenSourceMalware calls the list a snapshot and it is not permanent. Remove: delete the ```optional: interim block list``` comment and the `in(dest_ip, ...)` line below it.
Sigma
Written for this site, from indicators reported by OpenSourceMalwareOne rule per telemetry type because a Sigma rule binds to one logsource; dead-drop values in a URL have no endpoint event, so they are matched in command lines only.
title: PolinRider History Rewrite, Propagation Script or Bootstrap Command
id: 9eb236cf-a69d-4884-91aa-d352c315f66c
status: experimental
description: >
Process starts for the PolinRider propagation script temp_auto_push.bat, node
running the fake font file fa-solid-400.woff2, a command line naming a Vercel
bootstrap host or a TRON or Aptos dead-drop value, and git amending a commit or
force-pushing with hooks bypassed.
references:
- https://github.com/OpenSourceMalware/PolinRider
- https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider
logsource:
category: process_creation
detection:
selection_batch:
CommandLine|contains: 'temp_auto_push.bat'
selection_font:
Image|endswith:
- '\node.exe'
- '/node'
CommandLine|contains: 'fa-solid-400.woff2'
selection_bootstrap:
CommandLine|contains:
- '260120.vercel.app'
- 'default-configuration.vercel.app'
- 'vscode-settings-bootstrap.vercel.app'
- 'vscode-settings-config.vercel.app'
- 'vscode-bootstrapper.vercel.app'
- 'vscode-load-config.vercel.app'
- 'TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP'
- 'TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG'
- '0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e'
- '0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3'
# optional: developers amend and force-push with hooks disabled as ordinary practice
git_amend:
Image|endswith:
- '\git.exe'
- '/git'
CommandLine|contains|all:
- '--amend'
- '--no-verify'
# optional: as above
git_force_push:
Image|endswith:
- '\git.exe'
- '/git'
CommandLine|contains|all:
- 'push'
- '-uf'
- '--no-verify'
condition: 1 of selection_* or git_amend or git_force_push
falsepositives:
- Developers who routinely amend and force-push with hooks disabled
level: high
---
title: PolinRider Files Written
id: fcea8d63-8762-4875-aeb5-30d10cd9db67
status: experimental
description: >
Creation of temp_auto_push.bat, the fake dictionary file spellright.dict
written by node, or files inside one of the seven lookalike Tailwind and
PostCSS packages OpenSourceMalware attributes to the campaign.
references:
- https://github.com/OpenSourceMalware/PolinRider
logsource:
category: file_event
detection:
selection_script:
TargetFilename|endswith:
- '\temp_auto_push.bat'
- '/temp_auto_push.bat'
selection_dict:
Image|endswith:
- '\node.exe'
- '/node'
TargetFilename|endswith:
- '\spellright.dict'
- '/spellright.dict'
selection_packages:
TargetFilename|re: '(?i)[\\/]node_modules[\\/](tailwindcss-style-animate|tailwind-mainanimation|tailwind-autoanimation|tailwind-animationbased|tailwindcss-typography-style|tailwindcss-style-modify|tailwindcss-animate-style)[\\/]'
# optional: config.bat is a generic name, it matters beside temp_auto_push.bat or in a .gitignore
selection_config_bat:
TargetFilename|endswith:
- '\config.bat'
- '/config.bat'
condition: 1 of selection_*
falsepositives:
- Unrelated projects that contain a config.bat
level: high
---
title: PolinRider Dead-Drop or Bootstrap Connection
id: 3a6c8e12-9f47-5b0d-8e25-7d1b4c9a6f38
status: experimental
description: >
node connecting to the TRON and Aptos services the PolinRider loader reads its
next stage from, or any process connecting to a Vercel bootstrap host or to an
address on the interim block list.
references:
- https://github.com/OpenSourceMalware/PolinRider
- https://opensourcemalware.com/blog/developer-guide-getting-over-polinrider
logsource:
category: network_connection
detection:
selection_bootstrap:
DestinationHostname|endswith:
- '260120.vercel.app'
- 'default-configuration.vercel.app'
- 'vscode-settings-bootstrap.vercel.app'
- 'vscode-settings-config.vercel.app'
- 'vscode-bootstrapper.vercel.app'
- 'vscode-load-config.vercel.app'
# optional: web3 developers query TRON and Aptos from node as a matter of course
selection_chain:
Image|endswith:
- '\node.exe'
- '/node'
DestinationHostname|endswith:
- 'trongrid.io'
- 'aptoslabs.com'
# optional: interim block list, a snapshot that OpenSourceMalware says is not permanent
selection_ip:
DestinationIp:
- '166.88.54.158'
- '198.105.127.210'
- '23.27.202.27'
- '154.91.0.103'
- '136.0.9.8'
- '166.88.4.2'
- '23.27.120.142'
- '202.155.8.173'
- '166.88.134.82'
- '188.43.33.249'
- '23.27.13.43'
condition: 1 of selection_*
falsepositives:
- Web3 development that uses the TRON or Aptos public services
level: medium
Lower-fidelity parts (4)
- the git amend and force-push clause — developers amend and force-push with hooks disabled as ordinary practice. Remove: delete the `# optional: developers amend` comments, the `git_amend` and `git_force_push` blocks, and `or git_amend or git_force_push` from the first rule's condition.
- the config.bat file name — a generic name that matters only beside temp_auto_push.bat or in a .gitignore. Remove: delete the `# optional: config.bat` comment and the `selection_config_bat` block in the second rule; `1 of selection_*` needs no change.
- node reaching trongrid.io or aptoslabs.com — web3 developers query TRON and Aptos from node as a matter of course. Remove: delete the `# optional: web3 developers` comment and the `selection_chain` block in the third rule; `1 of selection_*` needs no change.
- the eleven interim block-list addresses — OpenSourceMalware calls the list a snapshot and it is not permanent. Remove: delete the `# optional: interim block list` comment and the `selection_ip` block in the third rule; `1 of selection_*` needs no change.
CrowdStrike LogScale
Written for this site, from indicators reported by OpenSourceMalwareCovers the process and network lines only, because Falcon logs no event for writes of .bat, .dict or package files and its DNS events name no process.
// Falcon logs no event for writes of .bat, .dict or package files, and its DNS events name no process, so the file and node-only lines are partly out of reach here
(
#event_simpleName=/^(Synthetic)?ProcessRollup2$/
and (
CommandLine=/temp_auto_push\.bat/i
or (ImageFileName=/[\\\/]node(\.exe)?$/i and CommandLine=/fa-solid-400\.woff2/i)
or CommandLine=/(260120|default-configuration|vscode-settings-bootstrap|vscode-settings-config|vscode-bootstrapper|vscode-load-config)\.vercel\.app|TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP|TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG|0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e|0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3/i
// optional: developers amend and force-push with hooks disabled as ordinary practice
or (ImageFileName=/[\\\/]git(\.exe)?$/i and CommandLine=/--no-verify/i and (CommandLine=/--amend/i or (CommandLine=/push/i and CommandLine=/-uf/i)))
)
)
or (#event_simpleName=DnsRequest and DomainName=/(260120|default-configuration|vscode-settings-bootstrap|vscode-settings-config|vscode-bootstrapper|vscode-load-config)\.vercel\.app$/i)
// optional: web3 developers query TRON and Aptos as a matter of course, and the lookup is not limited to node here
or (#event_simpleName=DnsRequest and DomainName=/(trongrid\.io|aptoslabs\.com)$/i)
// optional: interim block list, a snapshot that rotates
or (#event_simpleName=NetworkConnectIP4 and in(RemoteAddressIP4, values=["166.88.54.158", "198.105.127.210", "23.27.202.27", "154.91.0.103", "136.0.9.8", "166.88.4.2", "23.27.120.142", "202.155.8.173", "166.88.134.82", "188.43.33.249", "23.27.13.43"]))
| groupBy([ComputerName, aid], function=[count(as=events), collect([CommandLine, DomainName, RemoteAddressIP4]), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)])
| sort(last_seen, order=desc)
Lower-fidelity parts (3)
- the git amend and force-push clause — developers amend and force-push with hooks disabled as ordinary practice. Remove: delete the `// optional: developers amend` line and the `or (ImageFileName=/[\\\/]git ...` line below it.
- node reaching trongrid.io or aptoslabs.com — web3 developers query TRON and Aptos from node as a matter of course. Remove: delete the `// optional: web3 developers` line and the `DomainName=/(trongrid ...` line below it.
- the eleven interim block-list addresses — OpenSourceMalware calls the list a snapshot and it is not permanent. Remove: delete the `// optional: interim block list` line and the `NetworkConnectIP4 and in(RemoteAddressIP4 ...` line below it.
Elastic
Written for this site, from indicators reported by OpenSourceMalwareEach signal is a boolean column and the four low-fidelity ones are labelled low fidelity in the result, so they can be dropped without touching the core conditions.
FROM logs-*
| WHERE @timestamp > NOW() - 30 days
| EVAL cmd = TO_LOWER(process.command_line), proc = TO_LOWER(process.name), fname = TO_LOWER(file.name), path = REPLACE(TO_LOWER(file.path), "\\\\", "/"), web = TO_LOWER(COALESCE(url.original, destination.domain, dns.question.name))
| EVAL f_proc = cmd LIKE "*temp_auto_push.bat*" OR (proc IN ("node", "node.exe") AND cmd LIKE "*fa-solid-400.woff2*") OR cmd LIKE "*260120.vercel.app*" OR cmd LIKE "*default-configuration.vercel.app*" OR cmd LIKE "*vscode-settings-bootstrap.vercel.app*" OR cmd LIKE "*vscode-settings-config.vercel.app*" OR cmd LIKE "*vscode-bootstrapper.vercel.app*" OR cmd LIKE "*vscode-load-config.vercel.app*" OR cmd LIKE "*tmfkqed7tjja5xnzjz2lep838vrzrs7map*" OR cmd LIKE "*txfxhuet9pjvu1bgvkbabres4yuc1ngzcg*" OR cmd LIKE "*0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e*" OR cmd LIKE "*0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3*"
| EVAL f_file = fname == "temp_auto_push.bat" OR (fname == "spellright.dict" AND proc IN ("node", "node.exe")) OR path RLIKE ".*/node_modules/(tailwindcss-style-animate|tailwind-mainanimation|tailwind-autoanimation|tailwind-animationbased|tailwindcss-typography-style|tailwindcss-style-modify|tailwindcss-animate-style)/.*"
| EVAL f_net = web LIKE "*260120.vercel.app*" OR web LIKE "*default-configuration.vercel.app*" OR web LIKE "*vscode-settings-bootstrap.vercel.app*" OR web LIKE "*vscode-settings-config.vercel.app*" OR web LIKE "*vscode-bootstrapper.vercel.app*" OR web LIKE "*vscode-load-config.vercel.app*" OR web LIKE "*tmfkqed7tjja5xnzjz2lep838vrzrs7map*" OR web LIKE "*txfxhuet9pjvu1bgvkbabres4yuc1ngzcg*" OR web LIKE "*0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e*" OR web LIKE "*0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3*"
| EVAL f_git = proc IN ("git", "git.exe") AND cmd LIKE "*--no-verify*" AND (cmd LIKE "*--amend*" OR (cmd LIKE "*push*" AND cmd LIKE "*-uf*"))
| EVAL f_config = fname == "config.bat"
| EVAL f_chain = proc IN ("node", "node.exe") AND (web LIKE "*trongrid.io*" OR web LIKE "*aptoslabs.com*")
| EVAL f_ip = TO_STRING(destination.ip) IN ("166.88.54.158", "198.105.127.210", "23.27.202.27", "154.91.0.103", "136.0.9.8", "166.88.4.2", "23.27.120.142", "202.155.8.173", "166.88.134.82", "188.43.33.249", "23.27.13.43")
| WHERE f_proc OR f_file OR f_net OR f_git OR f_config OR f_chain OR f_ip
| EVAL signal = CASE(f_proc, "process", f_file, "file", f_net, "network", "low fidelity")
| STATS events = COUNT(*), signals = VALUES(signal), paths = VALUES(path), commands = VALUES(cmd), first_seen = MIN(@timestamp), last_seen = MAX(@timestamp) BY host.name
| SORT last_seen DESC
| LIMIT 100
Lower-fidelity parts (4)
- the git amend and force-push clause — developers amend and force-push with hooks disabled as ordinary practice. Remove: delete the `| EVAL f_git` line and `OR f_git` from the `| WHERE` line.
- the config.bat file name — a generic name that matters only beside temp_auto_push.bat or in a .gitignore. Remove: delete the `| EVAL f_config` line and `OR f_config` from the `| WHERE` line.
- node reaching trongrid.io or aptoslabs.com — web3 developers query TRON and Aptos from node as a matter of course. Remove: delete the `| EVAL f_chain` line and `OR f_chain` from the `| WHERE` line.
- the eleven interim block-list addresses — OpenSourceMalware calls the list a snapshot and it is not permanent. Remove: delete the `| EVAL f_ip` line and `OR f_ip` from the `| WHERE` line.
Cortex XQL
Written for this site, from indicators reported by OpenSourceMalwareCounts matching events per host and telemetry type, with dead-drop values matched in command lines because XDR network events carry no URL.
config case_sensitive = false
| dataset = xdr_data
| filter (event_type = ENUM.PROCESS and (
action_process_image_command_line contains "temp_auto_push.bat"
or (action_process_image_name in ("node.exe", "node") and action_process_image_command_line contains "fa-solid-400.woff2")
or action_process_image_command_line ~= "(260120|default-configuration|vscode-settings-bootstrap|vscode-settings-config|vscode-bootstrapper|vscode-load-config)[.]vercel[.]app|TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP|TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG|0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e|0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3"
// optional: developers amend and force-push with hooks disabled as ordinary practice
or (action_process_image_name in ("git.exe", "git") and action_process_image_command_line contains "--no-verify" and (action_process_image_command_line contains "--amend" or (action_process_image_command_line contains "push" and action_process_image_command_line contains "-uf")))
))
or (event_type = ENUM.FILE and (
action_file_name = "temp_auto_push.bat"
or (action_file_name = "spellright.dict" and actor_process_image_name in ("node.exe", "node"))
or action_file_path ~= "node_modules[^a-z0-9_.-](tailwindcss-style-animate|tailwind-mainanimation|tailwind-autoanimation|tailwind-animationbased|tailwindcss-typography-style|tailwindcss-style-modify|tailwindcss-animate-style)[^a-z0-9_.-]"
// optional: config.bat is a generic name, it matters beside temp_auto_push.bat or in a .gitignore
or action_file_name = "config.bat"
))
or (event_type = ENUM.NETWORK and (
action_external_hostname ~= "(260120|default-configuration|vscode-settings-bootstrap|vscode-settings-config|vscode-bootstrapper|vscode-load-config)[.]vercel[.]app$"
or dns_query_name ~= "(260120|default-configuration|vscode-settings-bootstrap|vscode-settings-config|vscode-bootstrapper|vscode-load-config)[.]vercel[.]app$"
// optional: web3 developers query TRON and Aptos from node as a matter of course
or (actor_process_image_name in ("node.exe", "node") and (action_external_hostname ~= "(trongrid[.]io|aptoslabs[.]com)$" or dns_query_name ~= "(trongrid[.]io|aptoslabs[.]com)$"))
// optional: interim block list, a snapshot that rotates
or action_remote_ip in ("166.88.54.158", "198.105.127.210", "23.27.202.27", "154.91.0.103", "136.0.9.8", "166.88.4.2", "23.27.120.142", "202.155.8.173", "166.88.134.82", "188.43.33.249", "23.27.13.43")
))
| alter signal = if(event_type = ENUM.FILE, "file", if(event_type = ENUM.PROCESS, "process", "network"))
| comp count() as events, min(_time) as first_seen, max(_time) as last_seen by agent_hostname, signal
| sort desc last_seen
Lower-fidelity parts (4)
- the git amend and force-push clause — developers amend and force-push with hooks disabled as ordinary practice. Remove: delete the `// optional: developers amend` line and the `or (action_process_image_name in ("git.exe", "git") ...` line below it.
- the config.bat file name — a generic name that matters only beside temp_auto_push.bat or in a .gitignore. Remove: delete the `// optional: config.bat` line and the `or action_file_name = "config.bat"` line below it.
- node reaching trongrid.io or aptoslabs.com — web3 developers query TRON and Aptos from node as a matter of course. Remove: delete the `// optional: web3 developers` line and the `or (actor_process_image_name in ("node.exe", "node") and ... trongrid ...` line below it.
- the eleven interim block-list addresses — OpenSourceMalware calls the list a snapshot and it is not permanent. Remove: delete the `// optional: interim block list` line and the `or action_remote_ip in (...)` line below it.
osquery
Written for this site, from indicators reported by OpenSourceMalwareLists installed lookalike packages and temp_auto_push.bat under a home directory, and optionally build configs above 4,000 bytes at the same depth.
SELECT 'package' AS signal, path, name || '@' || version AS detail
FROM npm_packages
WHERE name IN ('tailwindcss-style-animate', 'tailwind-mainanimation', 'tailwind-autoanimation', 'tailwind-animationbased', 'tailwindcss-typography-style', 'tailwindcss-style-modify', 'tailwindcss-animate-style')
UNION ALL
SELECT 'file' AS signal, path, CAST(size AS TEXT) AS detail
FROM file
WHERE (path LIKE '/Users/%/%/%' OR path LIKE '/home/%/%/%' OR path LIKE 'C:\Users\%\%\%')
AND (filename = 'temp_auto_push.bat'
-- optional: oversized build configs, the loader grows a file of 80 to 200 bytes to about 5,000
OR (filename IN ('postcss.config.mjs', 'tailwind.config.js', 'eslint.config.mjs', 'next.config.mjs', 'vite.config.js', 'babel.config.js', 'App.js') AND size > 4000)
)
Lower-fidelity parts (1)
- the oversized build-config check — the size test also flags large legitimate configs and covers only one directory depth under a home folder. Remove: delete the `-- optional: oversized build configs` line and the `OR (filename IN (...) AND size > 4000)` line below it.
YARA
Written for this site, from indicators reported by OpenSourceMalwareThree rules for repository content: the injected config payload on either variant's markers, the history-rewrite batch script, and a tasks.json that runs on folder open.
rule PolinRider_Config_File_Payload
{
meta:
description = "PolinRider loader appended to a JavaScript config file, either published variant"
reference = "https://github.com/OpenSourceMalware/PolinRider"
strings:
$v1_marker = "rmcej%otb%"
$v1_decoder = "_$_1e42"
$v1_global = "global['!']"
$v2_marker = "Cot%3t=shtP"
$v2_global = "global['_V']"
$v2_decoder = "MDy"
$g_require = "global['r']"
$g_module = "global['m']"
$padding = " "
condition:
filesize < 2MB and
(
$v1_marker or $v2_marker or
($padding and $g_require and $g_module and ($v1_decoder or $v1_global or $v2_decoder or $v2_global))
)
}
rule PolinRider_History_Rewrite_Batch
{
meta:
description = "temp_auto_push.bat: back-dates the clock, amends the last commit and force-pushes"
reference = "https://github.com/OpenSourceMalware/PolinRider"
strings:
$date_var = "LAST_COMMIT_DATE"
$time_var = "LAST_COMMIT_TIME"
$amend = "commit --amend"
$no_verify = "--no-verify"
$force_push = "push -uf origin"
condition:
filesize < 20KB and 4 of them
}
rule PolinRider_VSCode_Tasks_Json
{
meta:
description = "VS Code tasks.json that runs on folder open and references reported PolinRider bootstrap hosts, the fake font file or the template UUID"
reference = "https://github.com/OpenSourceMalware/PolinRider"
strings:
$folder_open = "folderOpen"
$uuid = "e9b53a7c-2342-4b15-b02d-bd8b8f6a03f9"
$font = "fa-solid-400.woff2"
$host1 = "260120.vercel.app"
$host2 = "default-configuration.vercel.app"
$host3 = "vscode-settings-bootstrap.vercel.app"
$host4 = "vscode-settings-config.vercel.app"
$host5 = "vscode-bootstrapper.vercel.app"
$host6 = "vscode-load-config.vercel.app"
condition:
filesize < 64KB and $folder_open and any of ($uuid, $font, $host*)
}
False positives and tuning
Amending and force-pushing with hooks disabled is an ordinary if careless workflow, so the git clause also returns people; a process launched through temp_auto_push.bat is not a developer tidying a branch.
The config.bat name is common; a file event for it matters in a repository root beside temp_auto_push.bat, or when .gitignore names it.
Web3 developers query TRON and Aptos from node as a matter of course, so scope that clause to machines outside that work or drop it.
The string matches key on markers that have already rotated once, and the size heuristic flags large legitimate configs; a clean result is a first pass, so check git reflog and GitHub's force-push audit entries.
Sources
- PolinRider: DPRK Threat Actor Implants Malware in Hundreds of GitHub Repos — OpenSourceMalware, 2026-04-11
- A Developer's Guide to Getting Rid of PolinRider — OpenSourceMalware, 2026-08-13