high Threat analysis

semantic-types PyPI Solana Keypair Monkey Patch

Socket reported that semantic-types became malicious at version 0.1.5 and 0.1.6, with five Solana-themed PyPI packages pulling it transitively. The payload monkey-patched solders.keypair.Keypair constructors, encrypted Solana private keys with an RSA-2048 public key, and exfiltrated ciphertext through Solana Devnet SPL memo transactions.

#pypi#supply-chain#solana#cryptocurrency#monkey-patching
On this page 0% read

    Executive Summary

    Socket reported a PyPI campaign by the alias cappership in which semantic-types carried the malicious payload and five Solana-themed packages pulled it transitively: solana-keypair, solana-publickey, solana-mev-agent-py, solana-trading-bot, and soltrade [Source 1].

    The malicious semantic-types update landed at 0.1.5 on January 26, 2025; 0.1.6 repackaged the same payload on January 28, 2025 [Source 1]. The payload monkey-patched solders.keypair.Keypair.from_seed, from_bytes, and from_base58_string, encrypted captured private key bytes with a hardcoded RSA-2048 public key, and sent the ciphertext as an SPL memo transaction through Solana Devnet RPC at api.devnet.solana.com [Source 1].

    There is no conventional attacker C2 domain to block for the key theft path. The hard indicators are package names/versions, PyPI publisher metadata from the Socket report, the Solana Devnet RPC endpoint, the SPL Memo program ID, the actor public key D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko, the RSA public-key fingerprint 5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62, and code paths that generate Solana keypairs while the malicious package is present [Source 1].

    Key Facts

    event_type: "malicious PyPI package with transitive dependency delivery"
    ecosystem: "PyPI"
    publisher:
      alias: "cappership"
      email: "cappership@proton.me"
    malicious_payload_package:
      name: "semantic-types"
      malicious_versions:
        - "0.1.5"
        - "0.1.6"
    transitive_carrier_packages:
      - "solana-keypair"
      - "solana-publickey"
      - "solana-mev-agent-py"
      - "solana-trading-bot"
      - "soltrade"
    execution_trigger: "Python import that registers monkey patches, followed by solders Keypair constructor use"
    patched_methods:
      - "solders.keypair.Keypair.from_seed"
      - "solders.keypair.Keypair.from_bytes"
      - "solders.keypair.Keypair.from_base58_string"
    collection_window_utc:
      start: "2025-01-26T00:00:00Z"
      end: "2025-05-29T23:59:59Z"
    network_iocs:
      - "api.devnet.solana.com"
    solana_iocs:
      memo_program_id: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
      actor_public_key: "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
    crypto_iocs:
      rsa_public_key_fingerprint_sha256: "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62"
    credentials_at_risk:
      - "Solana private keys created or imported through patched solders Keypair constructors"

    Source Confidence & Evidence Mapping

    • confirmed: Socket identified semantic-types as the core malicious package and solana-keypair, solana-publickey, solana-mev-agent-py, solana-trading-bot, and soltrade as dependent carrier packages [Source 1].
    • confirmed: Socket states semantic-types 0.1.5 introduced the malicious payload on January 26, 2025 and 0.1.6 repackaged it on January 28, 2025 [Source 1].
    • confirmed: Socket identified the monkey-patched methods Keypair.from_seed, Keypair.from_bytes, and Keypair.from_base58_string, the Solana Devnet RPC endpoint, the threat actor public key, the PyPI alias/email, and RSA public-key fingerprint [Source 1].
    • unclear: Public sources do not name victim wallets or prove which Devnet memo transactions correspond to real production keypairs.
    • not_observed: No evidence indicates compromise of the legitimate solders package itself.

    Impact Determination

    ClassificationCriteriaEvidence to collectHandling decision
    Confirmed compromiseA malicious package was installed and a patched solders.keypair.Keypair constructor generated or imported a real Solana keypair.Installed package/version evidence, code path invoking Keypair.from_seed, Keypair.from_bytes, or Keypair.from_base58_string, Solana Devnet RPC sendTransaction, memo program use, actor public key D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko.Treat keypairs created or imported through the process as exposed; move funds and authority to keys generated in a clean environment.
    Presumed exposedsemantic-types==0.1.5 or 0.1.6, or any carrier package, was installed in an environment that runs Solana key generation/import code, but runtime telemetry is missing.pip freeze, lockfile, package cache, source grep for patched methods, notebook history, CI output.Inventory all keypairs generated or imported by that environment after January 26, 2025 and replace them from clean tooling.
    Potentially exposedA carrier package appears in manifests or lockfiles, but installation or malicious dependency resolution is incomplete.Dependency graph, package proxy records, lockfile history, virtualenv/container inventory.Collect package-cache and environment evidence until the asset is classified.
    Not exposedNo malicious package, carrier package, affected version, Solana keypair constructor, Devnet RPC, memo program, or actor key appears in source, environments, caches, or telemetry.Negative repository search, package inventory, dependency lock, virtualenv/container search, and network telemetry search.Keep negative evidence with the case record and close this event for the asset.
    UnknownPackage inventory, dependency resolution history, endpoint data, source history, or Solana RPC telemetry is unavailable.Named telemetry gap with system, owner, and retention status.Keep Solana key material in scope until the missing evidence is recovered or key replacement is accepted as the closure path.

    Minimum Evidence To Collect

    package_evidence:
      - "semantic-types==0.1.5"
      - "semantic-types==0.1.6"
      - "solana-keypair"
      - "solana-publickey"
      - "solana-mev-agent-py"
      - "solana-trading-bot"
      - "soltrade"
    code_evidence:
      - "solders.keypair.Keypair.from_seed"
      - "solders.keypair.Keypair.from_bytes"
      - "solders.keypair.Keypair.from_base58_string"
    network_evidence:
      - "api.devnet.solana.com"
      - "sendTransaction"
      - "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
      - "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
    crypto_evidence:
      - "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62"

    Timeline

    • 2024-12-22: Socket reports benign semantic-types 0.1.2 and solana-trading-bot 0.1.0 were published [Source 1].
    • 2024-12-23: Socket reports updates for semantic-types 0.1.4, solana-trading-bot 0.1.1, and soltrade 0.1.1 with dependencies still benign [Source 1].
    • 2025-01-26: Socket reports semantic-types 0.1.5 introduced the malicious monkey-patching payload; solana-mev-agent-py 0.1.0 and solana-keypair 0.1.0 were also published [Source 1].
    • 2025-01-28: Socket reports semantic-types 0.1.6 repackaged the same malicious payload [Source 1].
    • 2025-02-04: Socket reports solana-keypair 0.2.1 and solana-publickey 0.2.1 were released and imported semantic-types [Source 1].
    • 2025-05-29: Socket published the public analysis with IOCs and the package cluster [Source 1].

    What Happened

    The actor used dependency relationships rather than a one-package-only lure. semantic-types contained the payload, while five Solana-themed packages depended on it and served as delivery paths [Source 1].

    Once imported, the payload modified methods on solders.keypair.Keypair at runtime. Calls to from_seed, from_bytes, and from_base58_string still returned a usable keypair, but the wrapper also sent key material to the attacker’s collection path through a background thread [Source 1].

    The exfiltration path used normal Solana Devnet RPC. Captured private key bytes were encrypted with the actor’s RSA public key, base64 encoded, embedded in an SPL Memo transaction, and broadcast to Devnet. The attacker could later read memo transactions associated with the actor public key and decrypt ciphertext offline [Source 1].

    Technical Analysis

    Package Manipulation

    payload_package: "semantic-types"
    malicious_versions:
      - "0.1.5"
      - "0.1.6"
    carrier_packages:
      - "solana-keypair"
      - "solana-publickey"
      - "solana-mev-agent-py"
      - "solana-trading-bot"
      - "soltrade"
    dependency_trigger: "pip resolves semantic-types from carrier package dependencies"

    Runtime Hook

    The hook targets the solders keypair class object already used by Solana Python developers. Monkey patching means source code that imports solders can look normal while runtime method dispatch has changed inside the interpreter [Source 1].

    Exfiltration

    transport:
      protocol: "Solana JSON-RPC over HTTPS"
      endpoint: "https://api.devnet.solana.com"
      rpc_method: "sendTransaction"
      program: "SPL Memo"
      memo_program_id: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
      actor_public_key: "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
    payload_encoding:
      key_material: "64-byte private key material from Keypair bytes"
      encryption: "RSA-2048 public key"
      encoding: "Base64 ciphertext in memo data"

    Affected Assets and Blast Radius

    affected_assets:
      ecosystems:
        - "PyPI"
      packages:
        - "semantic-types==0.1.5"
        - "semantic-types==0.1.6"
        - "solana-keypair"
        - "solana-publickey"
        - "solana-mev-agent-py"
        - "solana-trading-bot"
        - "soltrade"
      environments:
        - "developer virtualenvs"
        - "CI jobs"
        - "Jupyter notebooks"
        - "container images"
        - "Solana bots and trading tools"
      secret_material:
        - "Solana private keys generated by patched methods"
        - "Solana private keys imported through patched methods"
    not_currently_known_to_affect:
      - "the solders package itself"
      - "Solana keypairs generated in clean environments without the malicious packages"

    Indicators of Compromise

    package_versions:
      - "semantic-types==0.1.5"
      - "semantic-types==0.1.6"
      - "solana-keypair"
      - "solana-publickey"
      - "solana-mev-agent-py"
      - "solana-trading-bot"
      - "soltrade"
    domains:
      - "api.devnet.solana.com"
    urls:
      - "https://api.devnet.solana.com"
    solana:
      memo_program_id: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
      actor_public_key: "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
    crypto:
      rsa_public_key_fingerprint_sha256: "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62"
    python_symbols:
      - "solders.keypair.Keypair.from_seed"
      - "solders.keypair.Keypair.from_bytes"
      - "solders.keypair.Keypair.from_base58_string"

    Detection and Hunting

    Script: Python dependency, Solana key-generation, and Devnet RPC sweep

    #!/usr/bin/env python3
    import json
    import os
    import subprocess
    import sys
    import urllib.request
    from pathlib import Path
    
    ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
    LOG_ROOT = Path(os.environ.get("LOG_ROOT", "")).resolve() if os.environ.get("LOG_ROOT") else None
    OUT = Path(os.environ.get("OUT", "hp-semantic-types-solana-scope")).resolve()
    
    COLLECTION_START = "2025-01-26T00:00:00Z"
    COLLECTION_END = "2025-05-29T23:59:59Z"
    
    PACKAGES = [
        "semantic-types",
        "solana-keypair",
        "solana-publickey",
        "solana-mev-agent-py",
        "solana-trading-bot",
        "soltrade",
    ]
    SELECTORS = [
        "semantic-types==0.1.5",
        "semantic-types==0.1.6",
        "semantic-types",
        "solana-keypair",
        "solana-publickey",
        "solana-mev-agent-py",
        "solana-trading-bot",
        "soltrade",
        "solders.keypair",
        "Keypair.from_seed",
        "Keypair.from_bytes",
        "Keypair.from_base58_string",
        "api.devnet.solana.com",
        "sendTransaction",
        "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
        "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko",
        "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62",
    ]
    
    DEPENDENCY_FILES = {
        "requirements.txt",
        "requirements.lock",
        "pyproject.toml",
        "poetry.lock",
        "uv.lock",
        "Pipfile.lock",
        "environment.yml",
        "conda-lock.yml",
        "sbom.json",
        "cyclonedx.json",
    }
    
    # Positive signal: a malicious package/carrier package selector appears with solders Keypair constructor code or Solana Devnet RPC telemetry.
    # Escalation: malicious package evidence plus Keypair constructor use after COLLECTION_START requires replacement of Solana keys generated or imported in that process.
    
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "matches").mkdir(exist_ok=True)
    (OUT / "python-env").mkdir(exist_ok=True)
    (OUT / "solana-devnet").mkdir(exist_ok=True)
    selectors_file = OUT / "semantic-types-selectors.txt"
    selectors_file.write_text("\n".join(SELECTORS) + "\n")
    
    def scan_tree(base: Path, label: str) -> list[dict]:
        findings = []
        if not base.exists():
            return findings
        skip = {".git", "node_modules", ".venv", "venv", "__pycache__", "dist", "build"}
        for root, dirs, files in os.walk(base):
            dirs[:] = [d for d in dirs if d not in skip]
            for name in files:
                p = Path(root) / name
                if p.stat().st_size > 50_000_000:
                    continue
                try:
                    data = p.read_text(errors="ignore")
                except Exception:
                    continue
                hits = [s for s in SELECTORS if s in data]
                if hits:
                    findings.append({"scope": label, "path": str(p), "hits": sorted(set(hits))})
        return findings
    
    findings = scan_tree(ROOT, "root")
    if LOG_ROOT:
        findings.extend(scan_tree(LOG_ROOT, "log_root"))
    
    (OUT / "matches" / "selector-matches.jsonl").write_text(
        "".join(json.dumps(x, sort_keys=True) + "\n" for x in findings)
    )
    (OUT / "matches" / "dependency-file-hits.jsonl").write_text(
        "".join(json.dumps(x, sort_keys=True) + "\n" for x in findings if Path(x["path"]).name in DEPENDENCY_FILES)
    )
    for item in findings:
        print(f"[MATCH] {item['scope']} {item['path']} :: {', '.join(item['hits'])}")
    
    for pkg in PACKAGES:
        res = subprocess.run(["python3", "-m", "pip", "show", pkg], text=True, capture_output=True)
        (OUT / "python-env" / f"pip-show-{pkg}.txt").write_text(res.stdout + res.stderr)
    
    rpc_payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "getSignaturesForAddress",
        "params": [
            "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko",
            {"limit": 1000},
        ],
    }
    req = urllib.request.Request(
        "https://api.devnet.solana.com",
        data=json.dumps(rpc_payload).encode(),
        headers={"Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(req, timeout=20) as resp:
            (OUT / "solana-devnet" / "actor-key-signatures.json").write_bytes(resp.read())
    except Exception as exc:
        (OUT / "solana-devnet" / "actor-key-signatures.error.txt").write_text(str(exc))
    
    if findings:
        print(f"[!] {len(findings)} selector-bearing files written under {OUT}")
    else:
        print(f"[+] No semantic-types/Solana incident selectors found under {ROOT}")

    Downstream Abuse Audits

    Script: Solana key-use and wallet replacement scoping collector

    #!/usr/bin/env python3
    import json
    import os
    import subprocess
    import sys
    from pathlib import Path
    
    ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
    OUT = Path(os.environ.get("OUT", "hp-semantic-types-solana-downstream")).resolve()
    
    SINCE = "2025-01-26T00:00:00Z"
    UNTIL = "2025-05-29T23:59:59Z"
    SELECTORS = [
        "semantic-types==0.1.5",
        "semantic-types==0.1.6",
        "solana-keypair",
        "solana-publickey",
        "solana-mev-agent-py",
        "solana-trading-bot",
        "soltrade",
        "Keypair.from_seed",
        "Keypair.from_bytes",
        "Keypair.from_base58_string",
        "solders.keypair",
        "api.devnet.solana.com",
        "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
        "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko",
    ]
    
    # Positive signal: repository, notebook, CI output, or package inventory contains malicious package evidence and Solana Keypair constructor use after SINCE.
    # Remediation trigger: any real wallet key generated or imported through a matched runtime must be replaced from a clean environment, and authority/funds must move to the replacement key.
    
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "git").mkdir(exist_ok=True)
    (OUT / "matches").mkdir(exist_ok=True)
    selectors_file = OUT / "selectors.txt"
    selectors_file.write_text("\n".join(SELECTORS) + "\n")
    
    def run(cmd: list[str], outfile: Path, cwd: Path | None = None) -> None:
        res = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True)
        outfile.write_text(res.stdout + res.stderr)
    
    run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(ROOT)], OUT / "matches" / "selector-search.txt")
    run(["rg", "-n", "Keypair\\.(from_seed|from_bytes|from_base58_string)|solders\\.keypair|solana_keypair|solana-publickey", str(ROOT)], OUT / "matches" / "keypair-code-paths.txt")
    run(["python3", "-m", "pip", "freeze"], OUT / "matches" / "pip-freeze.txt")
    
    if (ROOT / ".git").exists():
        run(["git", "log", "--since", SINCE, "--until", UNTIL, "--all", "--stat", "--", "requirements.txt", "pyproject.toml", "poetry.lock", "uv.lock", "Pipfile.lock"], OUT / "git" / "dependency-history.txt", ROOT)
        run(["git", "grep", "-n", "-e", "Keypair.from_seed", "-e", "Keypair.from_bytes", "-e", "Keypair.from_base58_string", "--", "*.py", "*.ipynb"], OUT / "git" / "keypair-git-grep.txt", ROOT)
    
    manifest = {
        "case": "semantic-types-solana-keypair-monkey-patch",
        "since": SINCE,
        "until": UNTIL,
        "remediation_trigger": "Replace Solana keys generated or imported by matched code paths while semantic-types 0.1.5 or 0.1.6 was installed.",
        "evidence_files": [
            str(OUT / "matches" / "selector-search.txt"),
            str(OUT / "matches" / "keypair-code-paths.txt"),
            str(OUT / "matches" / "pip-freeze.txt"),
            str(OUT / "git" / "dependency-history.txt"),
            str(OUT / "git" / "keypair-git-grep.txt"),
        ],
    }
    (OUT / "case-manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True))
    subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)])
    print(f"[+] Downstream artifacts written under {OUT}")

    Sources

    1. Socket: Monkey-Patched PyPI Packages Use Transitive Dependencies to Steal Solana Private Keys - Role: PRIMARY_RESEARCH - Impact: Package cluster, malicious versions, payload behavior, patched methods, Solana Devnet exfiltration, actor key, RSA fingerprint, and PyPI publisher identity.