Honeypot telemetry captured the deployment chain. Static reverse-engineering on the dropped scanner exposes its module name, source tree, and capability map. The loader is heavily obfuscated; the scanner is not.

Observation window: 2026-05-11 → 2026-05-14 (active during writing).

Primary signal: an SSH-delivered Linux kit observed on two of our honeypot sensors. Non-root victim sessions ended with a per-user systemd unit at ~/.config/systemd/user/watcher-netai.service, a persistence path that does not appear in public reporting on this loader as of search date 2026-05-14.

Actor confidence: behaviorally consistent with the cluster around the C2 IP 64.89.161.144 (URLhaus 3793559) and the operator-used HTTPS endpoints on connexionlost.net / connexionlost.zip. Romanian comments in the launcher script are a weak language signal, not an attribution.

New contribution: documents (a) the non-root systemd-user deployment outcome from a live SSH session; (b) the second-stage scanner skn (operator-internal module name recovered from Go buildinfo / DWARF, used as a working cluster label, not as a vendor-assigned family name) and its capability map: scanner, SOCKS5 proxy server, password-change cascade, embedded Go net/http listener, watchdog; (c) the current Stage-2 resolution 194.5.97.46 (AS149020 WebHorizon, Netherlands) that resolves the operator’s connexionlost.net and connexionlost.zip domains during our 2026-05 window; (d) the asymmetric obfuscation posture of the kit’s two binaries; (e) YARA, Sigma, and IOC bundles ready for community deployment.

Defensive value: high. Hunt paths we found no public report describing as of 2026-05-14, plus a reusable detection pattern for systemd-user persistence on multi-user Linux hosts.

📑 Companion: Watcher-NetAI - Detection Brief . Single-page SOC-facing summary: top IOCs, four triage-priority hunts, links to YARA / Sigma / IOC bundles, controlled-sharing contact.


Executive finding

A Linux loader that has been on VirusTotal for two months, with six engines flagging it, landed on two of our SSH honeypot sensors between May 11 and May 14. The binary, the C2 IP, and the SSH delivery URL are already public knowledge: listed on URLhaus, indexed on VirusTotal, matched by the THOR APT Scanner’s UPX-compressed-ELF rule. None of the public reports we have located name the family or document what the second stage actually is.

This report is what our honeypot sensors and static reverse-engineering let us add to that picture:

  • A non-root persistence path, ~/.config/systemd/user/watcher-netai.service, that lands on victims whose entry point was an unprivileged account. During targeted public-source searches performed on 2026-05-14 we did not identify prior public reporting documenting this user-systemd persistence path for this loader.
  • A scanner binary, dropped on victims as /dev/shm/astats, that is the second stage of the kit. Its module path is internally named skn, its 10 source files are visible by name through intact DWARF debug info, and its capability map covers a multi-role implant (scanner, credential validator, password-change cascade, SOCKS5 proxy server, embedded Go net/http listener, watchdog). Not a simple SSH bot.
  • A second operator-used infrastructure node, 194.5.97.46 (AS149020 WebHorizon, Netherlands), that resolves the connexionlost.net and connexionlost.zip domains and hosts the HTTPS endpoints the scanner is configured to call. During targeted prior-art searches performed on 2026-05-14 we did not identify prior public reporting tying these endpoints to this loader/scanner cluster.
  • An OPSEC asymmetry between the kit’s two binaries: the loader carries heavy Go-runtime obfuscation (custom .gopclntab magic, wiped buildinfo, runtime strings stripped or encrypted); the scanner does not (full DWARF, intact buildinfo, source-tree paths exposed).
  • A detection pack: five YARA rules, four Sigma rules, an IOC CSV, covering the chain from delivery to scanner C2.

The rest of the report develops each of these. The detection brief at the end gives the SOC-facing one-pager.


The kit at a glance

What our honeypots and static analysis let us draw:

SSH brute-force session against an unprivileged account
(operator → honeypot, ~30-second session)
            │
            ▼
loader delivered via curl/wget/dev-tcp fallback chain to
    ~/.sysmonitor  from  http://64.89.161.144:28816/[A-Za-z0-9_]{32}
(plaintext HTTP, AS205759 Ghosty Networks, Luxembourg; URLhaus 3793559)
            │
            ▼
On successful deployment, observed on disk after the SSH session ends:
- ~/.config/systemd/user/watcher-netai.service          (non-root persistence)
- ~/.config/systemd/user/default.target.wants/...       (enablement symlink)
- /tmp/w.sh   or   /dev/shm/w.sh                        (launcher script, 1413 B)
- /dev/shm/astats                                       (scanner binary, 10 MB)
- /dev/shm/s.lock   or   /var/run/s.lock                (single-instance lockfile)
            │
            ▼
Scanner astats runs long-lived (observed 12+ hours):
    /dev/shm/astats -watchdog ssh 2 ranges  (parent watchdog)
    /dev/shm/astats -scan     ssh 2 ranges  (child scan worker)
            │
            ▼
Scanner binary contains operational C2 configuration for HTTPS endpoints at:
    https://connexionlost.net:443
    https://connexionlost.zip:443
    Current resolution as of 2026-05-14: 194.5.97.46
    Paths: /update/scanfiles/{ranges,banIps,filters}.txt   (work-queue pulls)
           /update/pass/                                    (credential-reporting path)
           /harvest/{sshsv,users,sshlist}                   (recon POSTs)
    (resolved IP in AS149020 WebHorizon Internet Services, Netherlands)

Field observation: the non-root persistence path

Between 2026-05-11 18:20 UTC and 2026-05-14 06:52 UTC our sensors captured 33 SSH sessions where this loader (or a sibling under the same delivery URL) was downloaded. The brute-force vector is the standard one: password authentication against common low-privilege accounts. The username distribution across the captured sessions, in order of frequency: root (filtered, never given a shell on our sensors), admin, ubuntu, test, deploy, guest, testuser, user. None of those that reached a shell are root. Each session followed the same seven-step recon-and-deploy sequence, executing in approximately 34 seconds from authentication to disconnect.

We continued to observe matching command patterns during publication prep. Across multiple SSH honeypot regions, we recorded on the order of 69 Watcher-NetAI-specific command-pattern events across our sensor fleet in the seven days leading up to publication. The activity was still active in our sensor view as of 2026-05-14.

Operator infrastructure has a role separation

Across the live SSH sessions, we observed a split in operator-controlled source infrastructure. One source cluster repeatedly executes the loader-download command; a separate, mixed cloud-plus-residential set handles the persistence-setup commands (launcher writes, crontab edits, the systemd --user invocations, the ps grep astats heartbeat checks). The same SHA256 is at issue, but the SSH sessions that pull it and the SSH sessions that wire it into persistence on the victim are not the same source IPs.

We do not publish source IPs here because several appear to be compromised third-party systems, including residential and utility-sector networks. The point worth documenting is the split itself: a defender correlating on a single source IP underestimates the breadth of the operator’s fan-in.

The recon sequence

The operator’s first seven commands look like a hardware-and-context fingerprint, not interactive exploration:

uname -s -v -n -r -m
nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo
uptime 2>/dev/null | awk -F'( up |,|load)' '{gsub(/^[ \t]+|[ \t]+$/,"",$2); print $2}' | head -n1
awk -F: '/model name|Processor|Hardware/ {gsub(/^ +/, "", $2); print $2; exit}' /proc/cpuinfo
nvidia-smi -q | grep "Product Name" | head -n 1 | awk '{print $4, $5, $6, $7, $8, $9}' || echo 'No'
nvidia-smi -q | grep "Attached GPUs" | awk '{print $4}'
lspci | egrep VGA && lspci | grep 3D

A variant appears across 31 sessions: lspci | grep VGA | grep Radeon | wc -l | head -c 1. Many public Linux miner writeups emphasize NVIDIA-tooling probes such as nvidia-smi; the explicit Radeon count gives defenders an additional command-pattern pivot for this cluster. In our 2026-05-14 prior-art search we did not identify prior reporting that called out an AMD/Radeon-aware variant tied to this loader/scanner cluster.

The persistence step

Each session ends with two commands, executed back to back. The first writes the launcher to whichever directory the operator can use. The path-selection command is a literal shell cascade: /dev/shm first, then /tmp, then /var/run, /mnt, /root, and finally /, prioritising RAM-backed or world-writable locations before falling back to disk-backed paths. The launcher is then created in the first writable directory:

cd "/tmp" && if [ ! -f "w.sh" ]; then cat > "w.sh" && chmod +x w.sh; fi

The launcher script (w.sh) written here is documented in “What the launcher dispatches” below.

The second command is the persistence step. Recent field captures show the operator writing both persistence layers in sequence, not as alternatives: first a crontab @reboot / hourly launcher entry, then the user-systemd unit. The cron path is not merely a fallback for hosts where systemctl --user is unavailable; in observed sessions it is written alongside the user-systemd unit. The crontab write:

CRON="$(crontab -l 2>/dev/null || true)"
echo "$CRON" | grep -F '/tmp/w.sh "astats" "netai" "kstats" "ssh 2 ranges"' >/dev/null 2>&1 || \
  printf '%s\n%s\n' "@reboot  /tmp/w.sh [...]" \
                    "0 * * * * cd /tmp && ./w.sh [...]" | \
  (cat -; echo "$CRON") | crontab -

…followed by the user-systemd unit write:

sh -lc 'mkdir -p ~/.config/systemd/user && \
        cat > ~/.config/systemd/user/watcher-netai.service && \
        systemctl --user daemon-reload && \
        systemctl --user enable --now watcher-netai.service'

The unit file content:

[Unit]
Description=Watch netai via w.sh
After=network-online.target

[Service]
Type=simple
WorkingDirectory=/tmp
ExecStart=/tmp/w.sh "astats" "netai" "kstats" "ssh 2 ranges"
Restart=always
RestartSec=30

[Install]
WantedBy=default.target

Two minor variants exist, distinguished only by WorkingDirectory (/tmp vs /dev/shm) depending on which directory the earlier discovery step had picked. The content was identical across user accounts: a 238-byte file written under ~/<user>/.config/systemd/user/watcher-netai.service, with default.target.wants/ symlink populated by systemctl --user enable.

Three properties make this persistence approach interesting for defenders:

  1. It does not require root. No sudo, no polkit, no SUID. systemctl --user is available to any logged-in user and will trigger via linger if loginctl enable-linger is run subsequently, which we have not observed in field sessions.
  2. It does not land in /etc/systemd/, /usr/lib/systemd/, or /run/systemd/system/. Most current detection rules for systemd-based persistence on Linux target those three paths. ~/.config/systemd/user/ is rarely covered.
  3. It is named plausibly. “Watch netai via w.sh” reads like a developer-written sidecar; on a host that already runs custom services in the user’s home (which is increasingly common with rootless container patterns, podman, and similar), this unit blends in.

What to hunt this week

  • Primary hunt path. A query for ~/*/.config/systemd/user/*.service files created in the last 24 hours, owned by uids ≥ 1000, with ExecStart paths pointing to /tmp/, /dev/shm/, or other world-writable locations, is high signal. We expect low FP in fleets without heavy rootless-container use, and a triageable queue where rootless patterns (podman, user-mode services) are common; validate against your environment.
  • Hunt cron and user-systemd together. Field captures show the operator writing both layers in cascade, not as alternatives. A defender’s audit policy should watch both cron_user_write and ~/.config/systemd/user/ on the same session window; the appearance of one without the other on a recent compromise is itself a triage signal.

What the launcher dispatches

The launcher script’s logic clarifies what the four argument strings (astats, netai, kstats, "ssh 2 ranges") carry. They are role labels, not separate binaries: the launcher passes them to the dispatched scanner executable, which interprets each label as a mode of operation. The script picks between three roles based on architecture and core count:

  • amd64 + ≥4 cores → run miner only.
  • amd64 + <4 cores → run scanner only (probe more victims, leave the host’s CPU clean).
  • non-amd64 → run scanner and bot.

The launcher contains two Romanian-language comments, # DECIZIE: miner-only vs scan+bot and # doar scan, that we read as one signal of operator language preference. We do not attribute on the basis of a single comment, and the brute-force fleet in our session set includes nodes geolocated to a Swedish utility company’s network, a US residential ISP, a DigitalOcean instance in GB, and several VMs on AS51396 (Pfcloud UG, a NL hosting provider with a long track record of abuse complaints). The operator’s IP geography and their comment language disagree; we report both without resolving them.

On a successfully deployed host, the astats scanner ran for over 12 hours as the primary process tree:

deploy 7658 1     /dev/shm/astats -watchdog ssh 2 ranges
deploy 20857 7658 /dev/shm/astats -scan ssh 2 ranges

The watchdog parent re-spawns the scanner child; the "ssh 2 ranges" argument is the operator’s own label for the scan target list.


The loader binary

The loader binary is 712737cdde50fcce895f263ea18ed7f8e425dcb72f8ec63101ea0361c6602140: 4.2 MB UPX-packed, 13.7 MB after upx -d. The unpacking step is clean (UPX 4.2.2, standard stub); the obfuscation lives in the Go binary inside the UPX wrapper.

Three obfuscation observations stand out.

Custom .gopclntab magic

Go binaries carry a .gopclntab section whose first eight bytes start with a magic value that has been versioned in lock-step with the Go runtime: FB FF FF FF in Go 1.2, F0 FF FF FF in Go 1.16, and F1 FF FF FF in Go 1.18+ builds. This loader’s .gopclntab starts with:

D6 BB E4 9E 00 00 01 08 30 31 ...

Not a Go-standard magic value, and we have not seen it documented in public CTI literature as of search date 2026-05-14. The downstream effect: GoReSym (Mandiant) returns failed to locate pclntab; go version -m returns unknown; redress fails; every Go-aware decompiler we tried refused the binary.

This one bugged us for the better part of two days. The first reflex was a 4-byte XOR sliding key: applying the canonical Go 1.18 magic F1 FF FF FF against our D6 BB E4 9E and propagating produces no readable strings further into the section. A constant XOR is out. The transformation is something more involved (permutation, an RC4-like stream, a custom encoding), and we did not crack it in the time we had. If anyone in the community has seen the exact magic value D6 BB E4 9E 00 00 01 08 on another Go-obfuscated sample, we would like to hear about it.

Buildinfo wiped

go version -m is the canonical way to recover a Go binary’s module path, version, dependencies, and build flags. On this loader it returns unknown. The .go.buildinfo section is present (48 bytes) but its content has been zeroed or overwritten. There is no operator-internal name leaked at this level, unlike the scanner described below.

Runtime strings stripped or encrypted

strings -a on the unpacked loader returns about 9,375 lines. A standard Go binary typically carries several hundred lines that include identifiable Go runtime literals (runtime.gopark, runtime.chansend, runtime.mallocgc, garbage-collector pacing tables). The loader has five. Most Go runtime literals are either re-encoded or removed at compile time. What is also missing from cleartext: systemd, chronyd, journald, dbus-daemon, watcher-netai, .sysmonitor, astats, connexionlost, 64.89.161.144. The strings the loader needs at runtime are not visible by strings.

Garble , the most common public Go obfuscator, preserves the runtime literals. The loader does not preserve them. The toolchain is either Garble plus something else (a post-link rewrite step on the runtime), or a different obfuscator. We have not identified the specific toolchain.


The scanner: skn leaks the workshop

The scanner binary is a1297637c8c4300618bf407de15682e26c142ba2028338f101e045ec91f1073d: 10.0 MB statically linked Go ELF, GOAMD64=v1, CGO_ENABLED=0, built with go1.26.2 per intact .go.buildinfo. The binary is not stripped. The full DWARF debug section is present (about 948 KB compressed) and the Go symbol table is intact (.gopclntab 2.5 MB).

The buildinfo identifies the module path as skn in (devel) mode (i.e. built locally, not from a published Go module) with two third-party dependencies: github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b and golang.org/x/crypto v0.50.0.

Source-tree leak

After the time the loader cost us, the scanner was the opposite kind of binary. The first readelf -wi on it returned an intact DWARF section that names every source file in the operator’s build tree by absolute path. The operator was generous with us here. Those paths:

/root/.wrx/skn/az.go              — full-Internet random SYN scan mode
/root/.wrx/skn/config.go          — C2 endpoints + the HTTP auth token
/root/.wrx/skn/dns.go             — DNS resolution
/root/.wrx/skn/httpsv.go          — HTTP server (the implant listens)
/root/.wrx/skn/main_unix.go       — entrypoint, watchdog
/root/.wrx/skn/ranges.go          — operator-curated CIDR iteration
/root/.wrx/skn/socks5.go          — SOCKS5 proxy server
/root/.wrx/skn/ssh.go             — SSH credential validation, post-compromise
/root/.wrx/skn/synFunctions.go    — raw-socket SYN scanning primitives
/root/.wrx/skn/utils.go           — IP discovery, RNG, RLIMIT bump

That is the operator’s own naming of their project. The kit is internally skn. The launcher’s argv labels (astats, netai, kstats, "ssh 2 ranges") are role labels passed to the same binary, not separate executables; there is one scanner binary, not four. A string match for skn across 25 CTI corpora returned no prior naming as of 2026-05-14. Treating it as the cluster’s working name without naming the family is the option we recommend pending further evidence.

Capability map

Forty-two top-level main.* functions plus their funcN closures cover the following roles. The function-to-role mapping is derived from filename semantics and symbol names, exposed to anyone running objdump -d or readelf -s:

RoleFunctions (selected)What the role implies
SYN/TCP scanning (dual-mode)RunAZModeSyn (1989 B), RunAZModeTCP (886 B), RunRangesModeSyn (2166 B), RunRangesModeTCP (1339 B), ScanTargetWithSock (2375 B), FallbackToTCP (953 B), CheckSynPrivileges, CheckSynWorksRandom Internet-wide SYN scanning (AZ mode) or operator-curated CIDR scanning (Ranges mode). Checks for CAP_NET_RAW; falls back to TCP-connect probes when raw sockets are unavailable. The production argv astats -watchdog ssh 2 ranges / astats -scan ssh 2 ranges selects Ranges mode against SSH over operator-supplied ranges.
SSH credential validationsshcheck (1042 B), sshcheck.func1 (3114 B)Post-port-open, the scanner validates SSH credentials. The closure-style func1 size suggests an inner per-host loop.
Password-change cascadechangePasswordSmart (3501 B), newPassword (1098 B), sudoPath, sudoPasswordlessOnce authenticated, the scanner attempts to change the compromised account’s password, kicking the legitimate user off. Three-branch cascade: chpasswd → passwd → vyos, falling through on failure of the previous attempt. The VyOS branch is the third fallback - appliance-aware, not primary targeting.
SOCKS5 pivothandleSocks5Conn (4075 B - largest function in the binary), bindSocks5Listener, startSocks5ProxyAsync, sendSocks5ReplyThe binary implements SOCKS5 server logic (RFC 1928, identified by markers :1080, :443, tcp:22, ATYP/CMD constants in disassembly). bindSocks5Listener seeds math/rand from time.Now, calls Intn, then net.Listen - the listener port is randomised per victim rather than a fixed 1080. If exercised at runtime, this functionality would let the operator use a compromised host as routable infrastructure.
HTTP server (embedded)httpserver (725 B)httpserver registers route handlers with Go net/http and calls ListenAndServe. The binary implements an embedded HTTP listener path; this report does not characterise its live operator protocol or how the listener is reached in operation.
C2 communicationfetchData (2746 B), getData, sendreq (948 B), getGlobalHTTPClientThe fetch path. Uses Go stdlib net/http.NewRequestWithContext + (*Client).do. Authorization sent as HTTP header (not query param). InsecureSkipVerify referenced in adjacent code paths.
DNSresolveresolve calls Go net.LookupIP; we found no DoH, no DoT, and no hard-coded public resolver in that path. The scanner appears to use the victim’s configured resolver (/etc/resolv.conf).
Watchdog / single-instancestartWatchdog (970 B), startBackgroundWatchdog (847 B), scan (1052 B), backgroundLogic (2642 B), isLinuxAndNotFiltered, trySpawnGoroutineThe watchdog re-spawns the scanner child. juju/fslock is the package implementing the s.lock file observed in /dev/shm/ on the compromised host - single-instance enforcement. The kit refuses to run on non-Linux hosts.
UtilitiesGetOutboundIP (940 B), randStr, nolimits (RLIMIT_NOFILE), optimaltimeoutSelf-IP discovery (calls api.ipify.org), RNG helpers, file-descriptor limit raise (necessary for high-fanout scanning), timeout heuristic.

The handleSocks5Conn function, at 4075 bytes with cyclomatic complexity 84 and 135 basic blocks, is the largest recovered function by size and control-flow complexity.

Why the VyOS primitive matters

Inside the scanner binary’s plaintext strings sits this primitive:

configure; set system login user vyos authentication plaintext-password "%s"; commit; save; exit

It is the verbatim VyOS command sequence to add a system user account with a plaintext password. Static RE places it in changePasswordSmart as the third fallback in the cascade: the scanner first tries chpasswd, then passwd, then drops into VyOS configuration mode if the standard flow does not work. It is the kind of fallback an operator adds after discovering during testing that some compromised hosts (VyOS routers) do not have a working POSIX passwd because the user’s shell is a vendor CLI.

That still matters for defenders. The implant is not limited to generic Linux shells. It carries appliance-aware post-authentication logic and can persist account control on systems where user management happens through a network-device configuration CLI rather than /etc/passwd. VyOS deployments should not be treated as out of scope for Linux SSH botnet hunts: any user appearing in system login user configuration of recent commits (show configuration commands | match "set system login user") is worth checking, especially when preceded by failed shell-level password-change attempts in /var/log/auth.log.

Calling it “a Linux SSH scanner” undersells what is in the binary. The role mix is more like a scanner that also carries a SOCKS5 pivot, an HTTP listener, and an appliance-aware account-takeover path. The label matters because most public hunt rules for Linux SSH brute-forcers stop at the brute-force step. This binary’s interesting hours start after the brute-force succeeds.


C2 infrastructure

The kit’s C2 has two layers, on two distinct operator-controlled IPs in two ASNs and two countries.

Stage 1: loader delivery

The loader is delivered via plaintext HTTP from 64.89.161.144:28816. The delivery URL listed on URLhaus 3793559 is http://64.89.161.144:28816/CZRmrtxnrNONBXhwfFeqjNfBrliNaShG. The same URL has appeared in operator-executed curl commands captured during the 2026-05 field window, more than 60 days after URLhaus first listed it. We treat the URL as a stable delivery indicator: the operator has not been observed to rotate it in our window. The IP belongs to AS205759 Ghosty Networks LLC (registered London EC1V 2024-12-29, routed via Luxembourg).

Stage 2: scanner operational C2

The scanner’s config.go and fetchData paths reference HTTPS endpoints on:

  • connexionlost[.]net - HTTPS on :443
  • connexionlost[.]zip - HTTPS on :443

Both domains resolve to 194.5.97.46 (AS149020 WebHorizon Internet Services, Netherlands). Both delegate DNS to Njalla nameservers (1-you.njalla.no, 2-can.njalla.in, 3-get.njalla.fo), a privacy-focused registrar. We treat this as an infrastructure tradecraft signal, not attribution.

C2 paths recovered from the scanner’s .rodata:

GET  https://connexionlost[.]net/update/scanfiles/ranges.txt    — operator-curated CIDR scan list
GET  https://connexionlost[.]net/update/scanfiles/banIps.txt    — exclusion list
GET  https://connexionlost[.]net/update/scanfiles/filters.txt   — additional filters
POST https://connexionlost[.]net/update/pass/                   — credential-reporting path
POST https://connexionlost[.]net/harvest/sshsv                  — host SSH-service detail
POST https://connexionlost[.]net/harvest/users                  — enumerated user accounts
POST https://connexionlost[.]net/harvest/sshlist                — adjacent SSH targets discovered post-auth

The /update/scanfiles/*.txt paths are referenced in the scanner’s fetchData code path; a T1bf08eb000 symbol identified during static RE (1bf08eb000 hex = 120,000,000,000 ns) suggests a ~2-minute refresh cycle. Static RE places /update/pass/ adjacent to the password-change workflow; we treat it as the credential-reporting path. The /harvest/* paths are named verbatim in the scanner’s .rodata for post-auth reconnaissance categories.

Authentication on the HTTPS endpoints is via a static HTTP-header token hard-coded in config.go. We do not publish the token value. Static-derived authentication material has no defensive use to publish, and emitting it would invite parasitic interactions with the operator’s infrastructure. The presence of static header authentication and its format (alphanumeric, hard-coded) is an analytic signal; the token value itself is withheld and is not a consumable public IOC.

The binary references InsecureSkipVerify and VerifyHostname in code paths adjacent to the HTTPS client, suggesting TLS certificate validation may be disabled or relaxed on the scanner’s HTTPS calls.

Infrastructure history (passive DNS)

Public passive DNS (VirusTotal) shows the operator has rotated the IP of the Stage-2 domains across multiple hosters in the last seven months.

connexionlost.zip - first observed resolution 2025-09; the older of the two domains:

DateIP
2025-0988[.]214[.]57[.]157
2025-11194[.]5[.]97[.]15 (same /24 as the eventual landing)
2026-04194[.]5[.]97[.]46 (current; consolidated with .net three days later)

connexionlost.net - first observed resolution 2025-12:

DateIP
2025-125[.]249[.]160[.]243
2026-01103[.]179[.]44[.]146
2026-04194[.]5[.]97[.]46 (current)

The rotation cadence is roughly one to three months per IP. The scanner binary references the domains in its DWARF strings, not the IPs, so the operator can rotate the Stage-2 IP via DNS without redeploying the scanner on victims. For defenders the practical implication is that Stage-2 IOCs should be the domains plus the current IP; older IPs retain value for retrospective hunts but should not be treated as “the operator’s IPs” indefinitely. Reverse-IP lookups on the earlier landing IPs show a bulletproof-hosting pattern (multiple unrelated tenants recycling the same IP over time, including campaigns from other operators); we therefore do not attribute neighbor hostnames on those IPs to the same operator without independent corroboration.

By contrast, 64.89.161.144 (Stage-1 delivery) has zero historical domain resolutions in VirusTotal’s passive DNS. The operator never publicly resolves a hostname to this IP; Stage-1 is raw-IP delivery without DNS. This is a deliberate OPSEC choice that resists domain-takedown on the noisiest part of the chain.

What passive DNS does not show

We use passive DNS only for operator infrastructure history and domain pivots. It does not show victims; resolver geography in passive-DNS sensors reflects sensor / recursive-DNS coverage, not infected-host geography. It does not show operator location (the privacy registrar anonymises WHOIS). It does not show C2 reachability state at the moment of resolution.

What this changes about the kit’s profile

  • The kit is not just a brute-force scanner. It is also a credential-exfiltration channel and a remote-controlled scan-configuration framework: the operator ships the scan plan (ranges.txt) to the kit, the kit ships back results (harvest/*).
  • The kit auto-updates its own targeting. The operator can change scan ranges or push a new exclusion list without redeploying any binary.
  • The infrastructure spans two ASNs. Delivery on AS205759 Ghosty Networks LU; operational on AS149020 WebHorizon NL. The two domains connexionlost.net and connexionlost.zip resolve to the same operational IP: a .zip TLD on a Google-owned TLD known for abuse, alongside a conventional .net.

OPSEC asymmetry: binaries and infrastructure

The two binaries are obfuscated differently. The contrast is the most operationally interesting feature of the kit:

Loader (.sysmonitor)Scanner (astats)
SHA256712737cdde50fcce…6602140a1297637c8c43006…f1f1073d
Size4.2 MB packed / 13.7 MB unpacked10.0 MB
FormatUPX-packed Go ELFGo ELF, statically linked
.gopclntab magicCustom D6 BB E4 9E 00 00 01 08 (not Go-standard)Standard F1 FF FF FF (Go 1.18+)
StripSection headers absent on unpacked binaryNot stripped - full DWARF
Go buildinfoWiped (go version -munknown)Intact (go1.26.2, module path skn, deps visible)
Operational stringsEncrypted at compile time, decrypted at runtimeCleartext in .rodata
Source paths leakedNoneAll 10 source files: /root/.wrx/skn/{az,config,dns,httpsv,main_unix,ranges,socks5,ssh,synFunctions,utils}.go
Go-aware RE toolingFails (GoReSym, redress, go version -m)Works

The loader sits in front of public threat intel platforms (URLhaus, VirusTotal, MalwareBazaar) and is the artefact a defender will encounter first. It is hardened against analysis. The scanner sits behind the deployment chain, on hosts the operator has already taken, and is reachable to a defender only after the kit has succeeded somewhere. It is essentially analyst-ready: the operator’s source-tree layout, module name, dependency list, and function-symbol map are all preserved.

The same asymmetry appears at the infrastructure level. Stage-1 delivery rides a raw IP with no DNS resolution, anti-takedown on the noisiest, most replayed part of the chain. Stage-2 operational C2 uses two DNS names behind a privacy registrar, on a rotating IP, so the operator can change the Stage-2 IP via DNS without touching the scanners already deployed on victims, because the scanner references the domains in its DWARF strings, not the IPs. Each layer’s infrastructure choice matches the protection requirement of the artefact it serves.

We do not speculate on the operator’s intent for this asymmetry. The defensive outcome is the same regardless: the scanner exposes the operator’s internal architecture, which is what makes the rest of this report possible.


Detection engineering

Five YARA rules, four Sigma rules, and an IOC CSV cover the chain:

Detection priorities

High-confidence (filename / process / arg-list, unique to this cluster):

  • File ~/*/.config/systemd/user/watcher-netai.service (any age, any uid)
  • File ~/*/.config/systemd/user/default.target.wants/watcher-netai.service (the enablement symlink)
  • Process astats invoked with -watchdog or -scan argv
  • Any process invocation matching the four-token argument list "astats" "netai" "kstats" "ssh 2 ranges"

Medium-confidence (pattern likely indicative; some legitimate noise on fleets using rootless containers):

  • Any systemd user unit ~/*/.config/systemd/user/*.service created in the last 24 hours with ExecStart pointing to /tmp, /dev/shm, or /var/tmp
  • New ~/*/.config/systemd/user/ directory owned by uid ≥ 1000 on a host where no rootless-container / podman / user-mode systemd pattern is expected
  • Cron entries containing @reboot or 0 * * * * invocations that reference /tmp/, /dev/shm/, or other world-writable launcher paths

Network, operational C2:

  • Destination IP 194.5.97.46
  • Destination domains connexionlost.net, connexionlost.zip
  • URL paths /update/scanfiles/*, /update/pass/, /harvest/{sshsv,users,sshlist} (pair with HTTPS / port 443)

Auditd (targeted watches, production-safe)

A blanket -w /home is too broad for production fleets. Watch only the persistence-relevant subpaths instead:

-w /home/admin/.config/systemd/user/   -p wa -k user_systemd_persist
-w /home/ubuntu/.config/systemd/user/  -p wa -k user_systemd_persist
-w /home/deploy/.config/systemd/user/  -p wa -k user_systemd_persist
-w /root/.config/systemd/user/         -p wa -k root_systemd_persist

On hosts with dynamic user creation, prefer a file-monitor tool that supports glob watches (fanotify-based, lsm rules, or Elastic Defend) over auditd path-watches.

FP-testing status

The YARA rules were tested against a benign Linux Go corpus (kubectl, gh, jq, yq, hugo, caddy, helm, minikube, kind, fzf, each from its project’s official release URL). Zero false positives across the five rules; true-positive match on each of the five intended targets. Defenders deploying in production EDR pipelines should validate against their own larger corpus.

The Sigma rules have not been corpus-validated; defenders should treat them as field-developed signatures awaiting community validation.


IOCs

Network

IndicatorTypeNotes
64[.]89[.]161[.]144Stage-1 C2 IP - loader deliveryAS205759 Ghosty Networks LLC (registered London EC1V 2024-12-29; routed via Luxembourg). URLhaus 3793559.
64[.]89[.]161[.]144:28816Stage-1 delivery port (HTTP)URL path is per-session random: /[A-Za-z0-9_]{32}
194[.]5[.]97[.]46Stage-2 current resolution as of 2026-05-14AS149020 WebHorizon Internet Services (Netherlands). Resolves connexionlost[.]net and connexionlost[.]zip.
194[.]5[.]97[.]15Stage-2 historical resolution (Nov 2025 - connexionlost.zip)Retrospective-hunt value. Same /24 as current. Bulletproof-recycled - not exclusive to this operator.
103[.]179[.]44[.]146Stage-2 historical resolution (Jan 2026 - connexionlost.net)Retrospective-hunt value. Bulletproof-recycled IP; no current operator activity confirmed.
88[.]214[.]57[.]157Stage-2 historical resolution (Sep 2025 - connexionlost.zip)Retrospective-hunt value. Earliest observed resolution of either domain.
5[.]249[.]160[.]243Stage-2 historical resolution (Dec 2025 - connexionlost.net)Retrospective-hunt value.
connexionlost[.]netStage-2 C2 domain (HTTPS)Configuration pull + credential-reporting path. DNS delegated to Njalla.
connexionlost[.]zipStage-2 C2 domain (HTTPS)Secondary, same current IP. DNS delegated to Njalla. Older registration than .net.
HASSH 0a07365cc01fa9fc82608ba4019af499SSH client fingerprint of brute-force fleet11+ source IPs share this; also a stock OpenSSH client signature - combine with destination context

Files

SHA256FilenameRoleNotes
712737cdde50fcce895f263ea18ed7f8e425dcb72f8ec63101ea0361c6602140.sysmonitorUPX-packed loaderFirst on VT 2026-03-09. 6/65 engines
7a71a17bde3611a1b147f8946106d6da4d859c0e322fb2bc437cc6c3eddb5390unpacked loaderpost-upx -dCustom .gopclntab magic D6 BB E4 9E 00 00 01 08
a1297637c8c4300618bf407de15682e26c142ba2028338f101e045ec91f1073dastatsscanner / SOCKS5 / pivot10 MB Go ELF, not stripped, full DWARF, module path skn
-w.shlauncher script, 1.4 KBRomanian comments, three-mode dispatch
-watcher-netai.servicesystemd user unit, 238 BNon-root persistence

Filesystem (post-compromise, observed on our honeypot sensors)

PathNotes
~/<user>/.config/systemd/user/watcher-netai.serviceNon-root persistence path
~/<user>/.config/systemd/user/default.target.wants/watcher-netai.serviceEnablement symlink
~/<user>/.sysmonitorLoader file (observed 0 bytes after the SSH session ends)
/tmp/w.sh, /dev/shm/w.shLauncher locations
/dev/shm/astats, /dev/shm/s.lockScanner binary + single-instance lockfile (juju/fslock)

Process

PatternNotes
/dev/shm/astats -watchdog ssh 2 rangesScanner watchdog parent
/dev/shm/astats -scan ssh 2 rangesScanner worker child (spawned by watchdog)
/tmp/w.sh "astats" "netai" "kstats" "ssh 2 ranges"Launcher, Type=simple ExecStart from the systemd unit

Static (binary)

IndicatorNotes
.gopclntab magic D6 BB E4 9E 00 00 01 08Custom, not Go-standard
Module path skn (Go buildinfo)Visible on the scanner; wiped on the loader
Go dependencies: github.com/juju/fslock, golang.org/x/crypto v0.50.0
Source paths in DWARF: /root/.wrx/skn/{az,config,dns,httpsv,main_unix,ranges,socks5,ssh,synFunctions,utils}.goOperator-internal layout
Embedded VyOS primitive: configure; set system login user vyos authentication plaintext-password "%s"; commit; save; exitIn changePasswordSmart, third fallback branch
Embedded launcher argv: "astats", "netai", "kstats", "ssh 2 ranges"Role labels, not module names

Limits and honest scope

Several things this report does not claim, and a defender should not read into:

  • A vendor-assigned family name. We searched 25 CTI sources for the artefact set (skn, /root/.wrx/skn/, watcher-netai.service, the four argv labels, the launcher signature, both C2 IPs, both C2 domains, the C2 paths, the embedded VyOS primitive, the HASSH fingerprint, both SHA256s, the Romanian comment tokens, and the adjacent IPs in AS205759 Ghosty Networks LLC and AS149020 WebHorizon). No combination returned a prior naming as of 2026-05-14. Thematic neighbours (PumaBot Darktrace 2025-05-28, SSHStalker Flare 2026-02-09, Outlaw / Dota Elastic 2025-04, Diicot Bitdefender / Wiz / Darktrace 2024-2025, Namz/SudoLite Flare 2026-01-08) share the broader theme but the IOC sets are disjoint and the persistence models differ. We treat skn and the cluster around it as one observable cluster of the broader Linux SSH brute-force ecosystem, with no claim of family-level novelty.
  • Operator identity. Two Romanian comments in a 76-line launcher script, plus a brute-force fleet sourced from at least four countries, plus two operator IPs in two different ASNs, do not constitute attribution.
  • The exact obfuscation algorithm on the loader’s .gopclntab magic. As noted above, a 4-byte XOR sliding key does not recover anything readable. The transformation is more involved, and we did not crack it.
  • The loader’s persistence-drop logic at the bytecode level. The loader is heavily obfuscated. The on-disk artefacts recovered after successful sessions establish what the loader produces; we did not attempt to characterise the binary’s internal control flow.
  • The scanner’s runtime behaviour at the protocol level. The capability map, function signatures, source-tree leak, and password-change cascade order are derived from static reverse-engineering of the scanner binary. The runtime semantics of the SOCKS5 listener, the embedded HTTP server, and the scan-and-harvest cycle are not characterised from controlled-environment execution.
  • What the C2 returned to the loader’s curl / wget calls. We did not capture the HTTP response body. The URL-stability observation above is evidence that the operator is still pointing victims at the same path; it is not direct evidence that the server is serving the same payload at each observation.
  • Victim geography or scale from passive DNS. Passive DNS for connexionlost.{net,zip} is useful for mapping operator infrastructure history and pivots, but it should not be read as victim telemetry. Resolver geography reflects sensor and recursive-DNS coverage more than infected-host geography.

What’s nailed down:

  • A Linux SSH brute-force kit observed on two of our honeypot sensors drops a non-root persistence path that public reporting on the loader does not call out.
  • The kit’s second-stage scanner is a multi-role implant (scanner, credential validator, password-change cascade, SOCKS5 proxy server, embedded Go net/http listener, watchdog), operator-internally named skn, with full source-tree paths visible through intact DWARF.
  • The kit’s C2 infrastructure spans two ASNs (AS205759 Ghosty Networks LU + AS149020 WebHorizon NL).
  • The two binaries of the kit carry asymmetric levels of obfuscation: the loader is heavily obfuscated, the scanner is not.
  • A defender-side YARA + Sigma + IOC pack is shippable today, with an FP sanity test on the YARA side and a “field-developed, validate locally” note on the Sigma side.

This is not a family-naming claim. It is a visibility claim.

Prior-art search method

On 2026-05-14 we searched public CTI / blog sources, URLhaus, VirusTotal relations and comments visible to us, MalwareBazaar-style hash pivots, GitHub / YARA / Sigma corpora, and general web search for the SHA256s of both binaries, the C2 IPs and domains, watcher-netai.service, /root/.wrx/skn/, skn, the four launcher argv labels (astats, netai, kstats, "ssh 2 ranges"), the embedded VyOS primitive, and the recovered Stage-2 C2 paths. We found thematic neighbours (listed in Sources cited) but no prior public report tying these artefacts into the scanner capability map and persistence path described here. The negative finding is bounded to that date and that search scope; subsequent or non-indexed reporting may exist.


Sources cited


Controlled sharing

This public report withholds material that would increase misuse risk or expose third-party infrastructure: raw authentication tokens, internal sensor metadata, source IPs of compromised third parties, and overly operational reproduction steps.

We retain the unredacted artefacts for responder use. Verified CERTs, law-enforcement teams, hosting-provider abuse desks, and incident responders working a confirmed case may request controlled access to extended C2 indicators, protocol notes, additional binary metadata, and the observed deployment timeline.

Contact: research@ohiiho.com. Include affiliation, case reference, and specific operational need.


The operator’s infrastructure remains active at the time of writing. The Detection Brief companion [Part 2/2] summarises the deployment IOCs and top hunts in single-page form for SOC use.