Blog
>
Cryptomining via exposed ComfyUI and Ollama Endpoints

Cryptomining via exposed ComfyUI and Ollama Endpoints

Seven days of reconnaissance, then eight hours to deploy an XMRig Monero miner across three payload variants. The last one hid a remote shell behind a health check. Eight days later a second wave killed and replaced them. Targets: exposed ComfyUI, Ollama, Jupyter, MLflow, Airflow, Gradio and Portainer. The source was a UK home connection, itself compromised.

Ratnesh Pandey, Japneet Singh, Varun Anand

Publication date : Sept 26 2026

From 23 August to 9 September 2026, one source address sent 651 requests to internet-facing AI services. All times are UTC. Pool figures are as of 11 September 2026, when the campaign was still mining.

Executive summary

  • Target. Exposed AI services - ComfyUI, Ollama, Jupyter, MLflow, Airflow, Gradio, and Portainer - through any endpoint that looked capable of accepting and running a string.
  • Scope. The same request sequence reached several unrelated internet-facing systems, usually within hours of each other, which is a broad sweep rather than an attack on a chosen target.
  • Objective. Along with unauthorized Monero mining, one of the four payload variants also carried an interactive shell.
  • Origin. We assess with moderate confidence that the campaign's origin, a UK residential address, is a compromised third-party host rather than attacker-owned infrastructure.
  • Reconnaissance. Seven days of it before any payload. The busiest day of the entire campaign, 29 August with 224 requests, delivered no payload: it went after credentials and checked whether the host was worth mining.
  • Access method. Over about eight hours, six unauthenticated paths that might run supplied text.
  • Staging. A shell command downloaded an unmodified XMRig build, first from GitHub's release CDN and later from the delivery host itself.
  • Execution. Variants 2 and 3 throttled the miner to one thread and a 25% CPU hint to stay under resource-utilization alerts. Variant 3 also disguised the process as a kernel worker thread and passed the response from a /health endpoint straight to eval, which turns it into an interactive command channel.
  • Generations. Two, eight days apart: 30–31 August, carrying three payload variants, and 8 September, carrying one.
  • Proceeds. 0.0116 XMR, roughly $5.91, never paid out.

A health check that answers with commands

On the wire, it looks like an ordinary monitoring agent. Every few seconds it calls /health, identifies itself as HealthCheck/2.1.0 (linux), and reports a number. Few paths look less interesting, and few user agents invite a second look.

Figure 1: Health-check beacon and command execution loop.

The number is the hashrate, taken directly from the miner’s log.

But the response is not used as a health check. It is treated as a command, passed to eval, and the result is sent back to the server.

At that point, this is no longer just cryptomining. It becomes an interactive shell running with the same privileges as the compromised service account. The miner simply keeps running between commands.

The polling interval is also randomized between three and ten seconds instead of using a fixed interval. This makes simple periodic beacon detection less reliable, so timing alone is unlikely to identify the channel.

We confirmed the channel is live and that both C2 hosts answered on the beacon path and accepted the campaign key. However, during our observation, we did not see the server send a command through it.

Figure 2: Campaign activity timeline, 23 August–9 September.

‍Seven days of reconnaissance, followed by eight hours of staging. 

The first contact was on 23 August. The first payload did not arrive until 30 August. During those seven days, no payload was delivered. But the operator was not idle. It repeatedly tried to get Jupyter to start a kernel or open a terminal, which would have given it code execution directly. Across the campaign, requests to /api/kernels and /api/terminals made up 289 of the 651 requests.

Where direct execution was not possible, it went after credentials instead. On 28 and 29 August it requested SSH keys, AWS credentials and /etc/shadow directly and through path traversal in MLflow, ComfyUI and Gradio, and sprayed default passwords at Portainer. It also asked exposed models to run nvidia-smi and read /proc/cpuinfo through Jupyter, checking whether the host was worth mining on.

Credential theft continued after the first payload. On 30 August it ran an Airflow credential spray, then queried /api/v1/connections?limit=1 to retrieve stored connection credentials. From 31 August it used Ollama’s /api/pull endpoint with path traversal strings targeting /root/.aws/credentials, /root/.ssh/id_rsa, and /etc/passwd, and kept doing so for nine days (Appendix D).

Figure 3: Seven days of reconnaissance before the first payload.
Three variants in eight hours
  • Variant 1 -  30 August, 19:50. Ten requests hit four different paths within five seconds. curl downloads the standard XMRig release from GitHub’s CDN, writes it to /tmp/xmrig, and runs it once. It connects to the mining pool over port 3333 with no encryption. There is no attempt to hide the process and no way to control it after launch.
  • Variant 2 - 30 August, 21:33. This arrived through a code-execution task API that we could not link to a specific product. It uses the same miner, but adds a restart loop. XMRig runs at nice -n 19, with one thread and a 25% CPU hint, and writes logs to /tmp/.xmr.log. It still uses port 3333, still makes no attempt to hide itself, and still has no control channel.
  • Variant 3 - from 31 August, 03:37. The miner moves to /tmp/.d/kw, inside a hidden directory. Pool traffic switches to TLS over port 443. The process takes the name of a kernel worker thread, and the command beacon described above is added. This is the point where the payload becomes more than a miner.

The goal stayed the same. The way the miner was deployed did not.

exec -a changes argv[0], so tools such as ps show the process as kworker/0:1H, the name used by a real Linux kernel worker thread. But this only changes the displayed name. It does not give the process kernel access. The process still has a file-backed executable, a working directory, and a network socket — things a real kernel thread does not have.

Figure 4: Three variants in one night, from stock miner to remote shell.

‍

We retrieved both delivered archives and analyzed them offline. We queried VirusTotal using only their hashes. Both files are byte-for-byte identical to published upstream XMRig releases (Appendix F).

The Ollama request could not have run

One of the six execution paths tells us more than the request count alone. At 03:37:43, the operator sent Variant 3 to Ollama’s model-creation endpoint inside a Modelfile:

Figure 5: Failed Ollama Modelfile command attempt.

‍

CMD is a Dockerfile instruction. It is not a valid Ollama Modelfile instruction. A Modelfile supports seven instructions: FROM, PARAMETER, TEMPLATE, SYSTEM, ADAPTER, LICENSE, and MESSAGE. Because CMD is unsupported, Ollama rejects the request and nothing is created.

That failed request is one of the most useful pieces of evidence in the capture. The operator appears to have seen a file format that looked similar to a container definition and tried to use it without understanding how Ollama worked.

That also tells us something about the target selection. The campaign was not narrowly looking for ComfyUI or Ollama installations. It was probing HTTP endpoints that might accept input and turn it into code execution.

The six paths gave us the count but this failed Ollama request gave us the evidence of the approach behind it.

Eight days later, staged from a residential host

The campaign returned on 8 September at 07:09. This time, where the payload came from is important.

The second-generation payload downloads the miner from am114.duckdns[.]org:7331.

That hostname resolved to the same IP address that sent all 651 requests in our capture: a Sky UK residential connection in Oxford. The host exposed an unauthenticated Ollama server and RDP and appeared to be running Windows 11 (Appendix E).

By this point, the campaign was using that residential host both to stage payloads and to receive beacons. The server still returns a cmd field, but the second-generation client discards the response, so it can no longer run commands. Based on the evidence available to us, it does not appear to be infrastructure owned by the operator.

The same payload was sent again over port 80 later that day.

Figure 6: Second-generation miner deployment script.

It also kills /tmp/.d/r and /tmp/.d/b when it starts. Those paths only appear in the second-generation script. That suggests a working copy had already run on other hosts.

The captured copies could not have created those files because this command is invalid:

Figure 7: Shell syntax error in the delivered command.

sh rejects &; as a syntax error before the command runs.

Figure 8: The second generation: louder, weaker, and broken.

Command and control

The first generation beaconed to beacon.ltdglobal[.]co[.]uk.

The domain was registered through Cloudflare on 21 March 2026, five months before we first saw it used. A rule looking only for newly registered domains would therefore have missed it.

Its origin server is hidden behind Cloudflare. That means a takedown request has one obvious recipient, and it is not the hosting provider. This is also the only piece of infrastructure in the campaign that appears to belong directly to the operator.

The second generation stopped using it. Instead, it used free dynamic DNS pointing to the residential host described earlier. Blocking that hostname is reasonable.

The campaign key

b3fc2a91d8e047f6a3b1cc5d appears unchanged in both generations. It survived eight days, a domain change, and a protocol change.

That makes it a campaign-level identifier rather than something tied to one deployment.

One identifier links pool earnings to a C2 session

Rig IDs follow this format:

<2 letters>-<word>-<word>-<4 hex>

We saw four:

  • cu-raze-rift-db68 — variants 1 and 3, delivered through ComfyUI
  • w5-dusk-crow-584b — variant 2
  • ol-dusk-crow-584b — variant 3, delivered through Ollama
  • cu-tarn-hook-4535 — second generation

The middle two reached the same system six hours apart through different endpoints. They differ only in the first two characters.

That suggests the prefix identifies the product surface; cu for ComfyUI and ol for Ollama, while the rest identifies the host.

The same identifier is then reused in three places: as the miner's --rig-id, as the pool password, and as the beacon URL path. That means one string links a compromised host's mining activity to its C2 session. That is unusually detailed per-victim tracking for commodity cryptomining.

None of these four identifiers appeared in the pool's worker list, which is expected: the payloads were captured, not executed.

A worker seen in an earlier capture of the pool's worker list follows the same naming scheme. That is what supports this interpretation.

Every artifact in the chain is legitimate on its own

Look at each part in isolation and very little stands out. The miner is a stock XMRig release downloaded from GitHub's own CDN. It is byte-for-byte identical to upstream.

The same hash has been submitted to VirusTotal under 70 different filenames, so neither the hash nor the local filename identifies this campaign.

Mining traffic goes to a public pool over TLS on port 443. The process calls itself kworker/0:1H, which is also the name of a legitimate Linux kernel worker thread. The C2 domain was registered five months before it was used.

The beacon looks like a /health request from something calling itself a monitoring agent. At any one layer, reputation checks have little to work with. The combination is what matters.

A download is piped into tar under /tmp, followed by chmod +x and execution. A process reports an argv[0] that does not match its executable.

A host that is not a monitoring agent starts making outbound /health requests carrying command-related parameters.

The useful detection question is therefore not just, "Is this artifact known to be bad?"

It is, "Does this combination of behaviour make sense?"

Up to six months of mining, for $5.91

The wallet appears directly in the payload, so the mining activity can be checked independently. The figures below came from SupportXMR's public API. The XMR price was captured from a public price endpoint in the same pass at 00:52:55Z on 11 September 2026.

The campaign was still mining at that point. Lifetime hashes alone cannot tell us when the campaign started. Dividing them by the current hashrate would assume that rate had remained constant the entire time.

The domain registration gives us a better upper bound.

beacon.ltdglobal[.]co[.]uk was registered on 21 March 2026, so the campaign could be at most 174 days old at the time of capture. That would imply an average rate of about 950 H/s, compared with the 2,127 H/s we measured.

Either way, the total work is far too high to fit inside the seventeen days between our first sighting and the capture date. The campaign was already running before we first saw it.

Earnings. The wallet had 0.011580 XMR outstanding, worth about $5.91 at $510.44/XMR.

Check it yourself. SupportXMR exposes three public endpoints that require no API key. Use the wallet from Appendix A:

Figure 9: Public SupportXMR endpoints used for wallet checks.

These values change continuously, so each figure is only valid for the timestamp at which it was captured.

Workers. Three identifiers were listed as recently active at 00:52:55Z:

Table 1: Recently active mining worker IDs and shares.
  • The worker list changes quickly. A capture nine hours earlier showed a different worker, which means at least four distinct hosts appeared within one day.
  • One host produced about 89% of the current hashrate, so most of the campaign’s output depended on a single victim at that moment.
  • One worker ID contains an IPv4 address, suggesting at least one victim exposed its own address in the identifier.
  • The active workers account for only 14.35% of lifetime hashes. Most of the campaign’s historical mining came from hosts that were no longer active when we checked. We cannot reliably estimate the total number of victims.

What defenders should do

Protect internet-facing execution endpoints.
The campaign did not need an exploit. It used exposed, unauthenticated services that accepted commands. Identify any internet-facing service that can execute user-controlled input and put authentication or network controls in front of it.

Alert on the reconnaissance.
The operator spent seven days probing before sending a payload. Requests to endpoints such as /api/kernels, empty token parameters, or attempts to read files like /proc/cpuinfo are strong signals when they come from the internet.

Restrict outbound traffic from AI hosts.
Inference servers usually have little reason to connect to mining pools, dynamic DNS hosts, or unknown external domains. Egress controls would have disrupted the payload download, mining traffic, and command channel.

Treat a miner as more than a mining incident.
One variant also provided remote command execution. If you find a miner, investigate what the service account could access and assume the incident may extend beyond cryptomining.

Rotate credentials on exposed hosts.
Where the operator could not execute code, it tried to read secrets. If an AI service was exposed to the internet, rotate any credentials stored on or accessible from that host.

How this was collected

Most of the investigation was passive. The activity described here is traffic observed to exposed AI services. We used Shodan and VirusTotal only for existing records, and queried VirusTotal by hash without uploading any samples. We also used public sources including RDAP, AbuseIPDB, DNS, and the SupportXMR API.

Appendix

A. Indicators of compromise
Primary identification of the campaign

The four rig IDs below are the per-host identifiers; the campaign key is the pivot that survives every rotation.

Table 2: Campaign identifiers and primary indicators.
Network indicators
Table 3: Network indicators and rig IDs.

The delivery platform is a probable victim and its address is masked throughout this report. Block the hostname am114.duckdns[.]org rather than the address behind it: that address is a home connection and it will change. Pool traffic runs to pool.supportxmr[.]com:3333 in the clear for generation 1 variants 1 and 2, and to :443 over TLS for variant 3 and generation 2.

Host artifacts
Table 4: Host file paths and their roles.
File hashes
Table 5: XMRig binary hashes and versions.
B. Event stream

651 requests, 23 August to 9 September 2026, across eight exposed systems, each receiving between 33 and 214 of them, grouped by API endpoint and intended purpose.

Table 6: Captured requests by endpoint and purpose.
C. Payload technical details
First generation: delivery events

30 August:

Table 7: First-generation delivery events on 30 August.

31 August:

Table 8: First-generation delivery events on 31 August.
The four variants, side by side

Everything in this table comes from the captured request bodies.

Table 9: The four payload variants compared.
Variant 3 in full

Staging. Idempotent: it will not re-download if kw already exists.

Figure 10: Variant 3 miner staging command.

Execution. The throttling is deliberate — one thread, a 25% CPU hint, RandomX light mode, huge pages off — and trades revenue for staying below resource-utilization alerts.

Figure 11: Variant 3 throttled miner execution loop.

The beacon loop is shown at the top of this report. Two things in it are worth stating outright. eval "$CMD" runs on whatever the server returns, and nothing in the loop constrains what that can be, so a host that ran this payload should be treated as having had hands-on access rather than merely a miner. And the jitter is in one expression, sleep $((RANDOM % 8 + 3)), which is what produces the three-to-ten-second interval.

What changed across generations, and what got louder
Table 10: Changes between payload generations.

The second generation is louder on most axes: no argv[0] masquerade, a 90% CPU ceiling in place of one throttled thread, plaintext HTTP. The exception is the beacon, which slows to a fixed thirty seconds. Whether that reflects a different operator, a rushed redeploy, or a judgment that stealth was not paying is not something we can determine from delivery alone.

D. Reconnaissance detail

Opening an execution channel. The campaign's largest activity by far. The operator repeatedly asked the Jupyter server to start a kernel or return a terminal: 142 of the /api/kernels requests are POSTs carrying a kernel spec ({"name": "python3"}, also python, ir, julia-1.0), and 32 of the /api/terminals requests are POSTs with an empty body. Three /api/kernels?token= requests test whether the server accepts an empty token. It also asked Jupyter's contents API for /proc/cpuinfo, which returns the core count: the number you check before deciding whether a host is worth mining on.

Table 11: Jupyter reconnaissance requests and purposes.

Credential theft. Where the operator could only read, it took secrets instead.

‍

Table 12: Credential theft targets and vulnerabilities.
E. The delivery host is likely a victim

We assess with moderate confidence that the host is a compromised intermediary rather than operator-owned infrastructure. Shodan's cached scan from 2026-08-24, one day after this activity began, records:

Table 13: Evidence about the probable victim host.

Four things support the victim assessment:

  • It is a residential line rather than hosted capacity.
  • Most of the 68 AbuseIPDB reports describe Docker, Redis and CouchDB probing (36, 12 and 6 respectively) that does not appear in the traffic we observed. More than one activity stream passes through this address, which points to a compromised machine rather than an operator's own.
  • The campaign's own second-generation staging and beacon resolve to it. An operator would not normally host a command channel on their own residential line.
  • The payloads could not have run here. This host is Windows 11 build 26100, and every captured mining payload is POSIX shell (/tmp/.d, pkill, uname -m, exec -a) that would not execute on it. Whatever compromised this machine is not in our capture.

How the host itself was compromised is not established. Because it is likely a victim, it is treated here as a platform rather than an identity, and its address is masked throughout.

F. The miner binary

The binary dropped by every variant is XMRig, a legitimate open-source Monero CPU miner and the most commonly abused miner in cryptojacking, which is why most scanners flag it as an unwanted program. Both archives we retrieved are byte-identical to published upstream releases: the bundled SHA256SUMS matches the shipped binary, and config.json is unmodified stock, still carrying "user": "YOUR_WALLET_ADDRESS" and the default donate pool. The wallet is supplied on the command line at runtime. The two generations dropped different versions.

Table 14: XMRig archive and binary comparison.

The filename is no better than the hash. VirusTotal flags 38 of 75 engines on the 6.26.0 build and 42 of 75 on 6.22.2, both as miner.xmrig/bitcoinmi, and it records the 6.26.0 binary under 70 different filenames across 132 submissions, including cpanel-updater, kernel_miner_bin, dns-filter, .sysupd, polkitd and wp-worker.exe. Several impersonate real Linux daemons or administrative tooling. Renaming stock XMRig to suit the target is evidently routine, which is what proves the hash identifies the tool rather than the incident. Only the runtime arguments and the network behavior discriminate: the wallet, the rig ID, the campaign key and the argv[0] mismatch.

G. Command and control
beacon.ltdglobal[.]co[.]uk (first generation)
Table 15: First-generation command-and-control domain details.

The domain sits behind Cloudflare and its origin is not exposed, so a takedown request has exactly one recipient, and that recipient is not a hosting provider. This is the one asset in the campaign that appears genuinely to belong to the operator.

Protocol behavior below is from one request per endpoint, made from a disposable, isolated VM using the rig IDs and key recovered from the captured payloads. No command was requested, and no sample was uploaded anywhere. The same isolated VM retrieved the second-generation archives, which are served only from the victim host.

Table 16: Health endpoint requests and responses.

One example of the health endpoint used by the payload to reach the C2 in this generation:

hxxps://beacon.ltdglobal[.]co[.]uk/health/<rig-id>?hr=<n>&k=b3fc2a91d8e047f6a3b1cc5d

In the URL, b3fc2a91d8e047f6a3b1cc5d is the campaign key and rig-id is the per-host identifier the miner also reports to the pool.

am114.duckdns[.]org (second generation)

The domain used as C2 by the second generation resolves to 151.231.x.x: the same address the payloads were delivered from, and the same address running the exposed Ollama server (Appendix E). It is served on ports 7331 and 80, and its tasking response was captured the same way: {"cmd":null,"ok":true}, where cmd: null is an idle state and ok: true confirms the key was accepted. The second-generation client never reads this response, so the field has no effect on hosts running it.

By the second generation the campaign is staging its payloads and receiving its beacons on a machine it does not appear to own.

hxxp://am114.duckdns[.]org:7331/health/<rig-id>?k=b3fc2a91d8e047f6a3b1cc5d&hr=<n>

The key and the rig-ID scheme are unchanged from the first generation. The practical consequence is that the campaign's infrastructure cost has fallen to a single domain registration, and that response options narrow: blocking am114.duckdns[.]org blocks a victim's home connection, and a takedown request has no hosting provider to send it to.

‍