CVE-2024-50387 & CVE-2024-50388

Catalpa 网络安全爱好者

本文复现 PWN2OWN 2024 比赛中涉及 QNAP NAS 的两个漏洞 CVE-2024-50387 & CVE-2024-50388

ARM64 架构,存在漏洞的程序可以点击这里下载。

CVE-2024-50387

根据官方公告,该漏洞影响 QNAP NAS 中的 SMB Service 插件,从 2024 年开始,QNAP 将原本内建在系统中的 SMB 服务剥离出来,成为一个单独的插件。该漏洞影响插件 4.15.x 版本,从历史更新来看,仅影响 4.15.001 和 h4.15.001 两个版本。

根据 ZDI 在比赛中发布的信息,可以知道该漏洞是参数注入 + SQL 注入。考虑插件本身的功能,它主要对外提供 Web 管理接口以及 SMB 服务,而 Web 管理接口是需要权限验证的,因此猜测漏洞存在于 SMB 服务中。而 Samba 是使用非常广泛的开源软件,原版程序不太可能出现此类问题,QNAP 可能对其进行了某些修改, 从而引入了这一个漏洞。

下载最新版的 SMB 插件,和旧版进行补丁对比,发现主要是一些 lib 库发生了变化,逐个分析很快就能找到疑似漏洞点位置,位于 libauth-samba4.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
// Old
__int64 __fastcall sub_A880(
int log_type,
const char *a2,
const char *a3,
const char *remote_machine_name,
int action_type,
const char *append)
{
const char *log_bin; // x3
const char *user; // x5
const char *IP; // x6
const char *computer_name; // x7
char v17[2048]; // [xsp+78h] [xbp+58h] BYREF

memset(v17, 0, sizeof(v17));
if ( action_type != 9 )
{
log_bin = "/sbin/conn_log_tool";
if ( a2 )
goto LABEL_3;
LABEL_11:
user = "Unknown";
if ( a3 )
goto LABEL_7;
LABEL_12:
IP = "0.0.0.0";
if ( remote_machine_name )
goto LABEL_8;
LABEL_13:
computer_name = "Unknown";
goto LABEL_9;
}
log_bin = "/usr/local/samba/sbin/log_ratelimit.sh";
if ( !a2 )
goto LABEL_11;
LABEL_3:
if ( *a2 == '\\' )
user = a2 + 1;
else
user = a2;
if ( !a3 )
goto LABEL_12;
LABEL_7:
IP = a3;
if ( !remote_machine_name )
goto LABEL_13;
LABEL_8:
computer_name = remote_machine_name;
LABEL_9:
snprintf(
v17,
0x800uLL,
"%s -t %d -u '%s' -p '%s' -m '%s' -i %d -n %d -a '%s' -S",
log_bin,
log_type,
user,
IP,
computer_name,
1,
action_type,
append);
smbrun(v17, 0LL, 0LL);
return 0LL;
}
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
// New
size_t __fastcall cmd_filter(const char *data)
{
size_t result; // x0
__int64 v3; // x2
bool v4; // zf
bool v5; // cc
int v7; // w1

result = strlen(data);
if ( result > 0 )
{
v3 = 0LL;
do
{
while ( 1 )
{
v7 = data[v3];
if ( (v7 - 45) > 0x4Du || v7 == '/' )
break;
v4 = (v7 - 59) <= 5u || v7 == '[';
v5 = !v4 && (v7 - 93) > 1u;
if ( !v5 || v7 == '`' )
break;
if ( result <= ++v3 )
return result;
}
data[v3++] = '*';
}
while ( result > v3 );
}
return result;
}

__int64 __fastcall sub_A920(
int log_type,
_BYTE *a2,
const char *a3,
_BYTE *remote_machine_name,
int action_type,
const char *append)
{
__int64 v12; // x0
const char *v13; // x20
__int64 v14; // x0
const char *computer_name; // x22
const char *log_bin; // x3
const char *user; // x5
const char *IP; // x6
__int64 v20; // x0
__int64 v21; // x0
char v22[2048]; // [xsp+78h] [xbp+58h] BYREF

memset(v22, 0, sizeof(v22));
if ( a2 && *a2 )
{
v20 = _talloc_tos("../../source3/smbd/qnap_conn_log.c:104");
v13 = talloc_strdup(v20, a2);
if ( v13 )
goto LABEL_4;
return 1LL;
}
v12 = _talloc_tos("../../source3/smbd/qnap_conn_log.c:101");
v13 = talloc_strdup(v12, "Unknown");
if ( !v13 )
return 1LL;
LABEL_4:
if ( action_type == 9 )
cmd_filter(v13);
if ( remote_machine_name && *remote_machine_name )
{
v21 = _talloc_tos("../../source3/smbd/qnap_conn_log.c:119");
computer_name = talloc_strdup(v21, remote_machine_name);
}
else
{
v14 = _talloc_tos("../../source3/smbd/qnap_conn_log.c:116");
computer_name = talloc_strdup(v14, "Unknown");
}
if ( computer_name )
{
cmd_filter(computer_name);
if ( action_type == 9 )
{
log_bin = "/usr/local/samba/sbin/log_ratelimit.sh";
if ( *v13 == '\\' )
user = v13 + 1;
else
user = v13;
if ( a3 )
goto LABEL_15;
}
else
{
log_bin = "/sbin/conn_log_tool";
if ( *v13 == 92 )
user = v13 + 1;
else
user = v13;
if ( a3 )
{
LABEL_15:
IP = a3;
LABEL_16:
snprintf(
v22,
0x800uLL,
"%s -t %d -u '%s' -p '%s' -m '%s' -i %d -n %d -a '%s' -S",
log_bin,
log_type,
user,
IP,
computer_name,
1,
action_type,
append);
smbrun();
_talloc_free(v13, "../../source3/smbd/qnap_conn_log.c:143");
_talloc_free(computer_name, "../../source3/smbd/qnap_conn_log.c:144");
return 0LL;
}
}
IP = "0.0.0.0";
goto LABEL_16;
}
_talloc_free(v13, "../../source3/smbd/qnap_conn_log.c:122");
return 1LL;
}

这个函数是用来记录登录日志的,当 SMB 服务收到一次认证请求,成功或失败,都会调用此函数向系统 Log 数据库插入一条信息,这条信息包括时间、登录用户、客户端名称等。在旧版中,代码获取了必要参数之后,直接将它们拼接到命令中,并使用 smbrun 函数执行。而新版在拼接命令之前对相关参数进行了验证,将一些特殊字符替换为星号。

smbrun 是 Samba 内置的一个函数,用来安全的执行一条命令:

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
/****************************************************************************
By default this now sanitizes shell expansion.
****************************************************************************/
int smbrun(const char *cmd, int *outfd, char * const *env)
{
return smbrun_internal(cmd, outfd, true, env);
}

static int smbrun_internal(const char *cmd, int *outfd, bool sanitize,
char * const *env)
{
// ...

/* close all other file descriptors, leaving only 0, 1 and 2. 0 and
2 point to /dev/null from the startup code */
closefrom(3);

{
char *newcmd = NULL;
if (sanitize) {
newcmd = escape_shell_string(cmd);
if (!newcmd)
exit(82);
}

if (env != NULL) {
execle("/bin/sh","sh","-c",
newcmd ? (const char *)newcmd : cmd, NULL,
env);
} else {
execl("/bin/sh","sh","-c",
newcmd ? (const char *)newcmd : cmd, NULL);
}

SAFE_FREE(newcmd);
}

/* not reached */
exit(83);
return 1;
}

默认情况下 sanitize 参数为 true,函数会调用 escape_shell_string 去过滤命令,将一些危险字符使用反斜杠转义,因此不能直接注入新的命令。但 smbrun 不会转义单引号等字符,所以我们可以传入一些包含单引号的数据,将命令中原本的引号闭合,这样就能控制后续执行程序的参数,实现参数注入。

通过调试分析发现,发送一条 SMB 登录请求会执行 /usr/local/samba/sbin/log_ratelimit.sh 脚本

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
#!/bin/sh

# If ratelimit is turn-off, simply send to Qulog and early exit.
ratelimit_bool="`/sbin/getcfg Samba "ratelimit bool" -d "TRUE" -u`"
if [ "x${ratelimit_bool}" != "xTRUE" ]; then
/sbin/conn_log_tool $@
exit "$?"
fi

# Get ratelimit seconds. default 3 seconds.
ratelimit_sec="`/sbin/getcfg Samba "ratelimit sec" -d "3" -u`"

# Init defaults.
ret=0 # return value
argv="$@" # all arguments.
ip= # ip address.
machine= # computer name.
username= # username. (might contain domain or workgroup)
dummy= # dummy arguments saved here for debug.

# Parse arguments and get client info.
POSITIONAL_ARGS=()

while [[ $# -gt 0 ]]; do
case $1 in
-p)
ip="$2"
shift # past argument
shift # past value
;;
-m)
computer="$2"
shift # past argument
shift # past value
;;
-u)
# If username's prefix contains domain as '<DOMAIN>\<USER>',
# replace backslash as semicolon, as '<DOMAIN>:<USER>' for valid grep.
username=`echo "$2" | sed 's/\\\\/:/g' 2>/dev/null`
shift # past argument
shift # past value
;;
-*)
# echo "Unknown option $1"
# exit 1
dummy="$2"
shift # past argument
shift # past value
;;
*)
POSITIONAL_ARGS+=("$1") # save positional arg
shift # past argument
;;
esac
done

# Generate keyword from client info.
keyword="${username}:${ip}:${computer}"

# DEBUG.
#echo ${keyword} >> /tmp/samba_log_ratelimit

# Lookup. Bypass if the same log is sent.
for pid in `pidof sleep`
do
# Check keyword, case insensitive.
grep -i "CMD=${keyword}" /proc/"${pid}"/environ 2>&1 >/dev/null
if [ x$? == x0 ]; then
# Found the same log is sent. Bypass.
exit 1
fi
done

# Send to Qulog. Insert a record for lookup. Run in background to avoid blocking parent.
(/sbin/conn_log_tool $argv && CMD="${keyword}" sleep "${ratelimit_sec}") &
ret="$?"

exit "${ret}"

实际上最后也是执行 /sbin/conn_log_tool,脚本只是为了限制发送日志的频率。执行的命令模式为

1
/usr/local/samba/sbin/log_ratelimit.sh -t 1 -u '<用户名>' -p '<客户端 IP 地址>' -m '<客户端主机名>' -i 1 -n 9 -a '---' -S

/sbin/conn_log_tool 是指向 /sbin/log_tool 的符号链接,分析 log_tool 程序,它会获取一系列参数,当包含 -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
// ...
else if ( append == 1 )
{
memset(v77, 0, sizeof(v77));
*&v77[4] = qword_41A560;
if ( qword_41A568 )
strncpy(&v77[64], qword_41A568, 0x40uLL);
if ( qword_41A570 )
strncpy(&v77[129], qword_41A570, 0x40uLL);
if ( qword_41A578 )
strncpy(&v77[194], qword_41A578, 0x40uLL);
if ( qword_41A5C0 )
strncpy(&v77[1556], qword_41A5C0, 0x40uLL);
if ( qword_41A5C8 )
strncpy(&v77[1621], qword_41A5C8, 0x80uLL);
if ( qword_41A5D0 )
strncpy(&v77[1750], qword_41A5D0, 0xFFuLL);
strncpy(&v77[259], qword_41A558, 0x400uLL);
*&v77[1284] = dword_41A580;
*&v77[1288] = dword_41A584;
*&v77[1552] = qword_41A5B8;
if ( append_msg )
strncpy(&v77[1292], append_msg, 0x100uLL); // [1]
if ( verbose )
puts("Appending a log to database...");
if ( log_engine == 1 ) // [2]
{
v66 = &v77[1750];
v67 = &v77[259];
v64 = &v77[1556];
v65 = &v77[1621];
v22 = SendConnToLogEngineEx4(); // [3]
}
else
{
v22 = (naslog_conn_add2)(v77); // [4]
}
if ( !verbose )
return 0;
}
// ...

[1] 处,判断是否传入了 -A 参数,如果是,就把这个参数值保存到结构体 v77 中。在 [2] 处,判断是否传入了 -S 参数,根据前面的命令模式知道正常情况下会进入这个 if 条件,从而执行 SendConnToLogEngineEx4 函数。

SendConnToLogEngineEx4 调用 msgsnd 向消息队列发送一条消息,最终会由 qLogEngined 进程来处理,观察到它会调用 naslog_conn_add_entries2 向数据库添加数据,这个函数定义在 libuLinux_naslog.so.2.0.0

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
// ...
memcpy(
v29,
"INSERT INTO NASLOG_CONN ( conn_type, conn_date, conn_time, conn_user, conn_ip, conn_comp, conn_res, conn_serv, conn_"
"action, conn_app, conn_action_result, conn_client_id, conn_client_app, conn_client_agent ) VALUES ( ?, strftime('%Y-"
"%m-%d', CURRENT_TIMESTAMP, 'LOCALTIME'), strftime('%H:%M:%S', CURRENT_TIMESTAMP, 'LOCALTIME'), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? );",
0x16AuLL);
v6 = 0LL;
if ( sqlite3_open("/etc/logs/conn.log", &v27) )
return 4294967276LL;
while ( 1 )
{
sqlite3_busy_timeout(v27, 20000LL);
if ( !sqlite3_prepare(v27, v29, 0xFFFFFFFFLL, &v28, 0LL) && !sqlite3_exec(v27, "BEGIN TRANSACTION;", 0LL, 0LL, 0LL) )
break;
sqlite3_close(v27);
if ( v6 == 6 )
return 4294967267LL;
if ( v6 )
{
++v6;
unlink("/etc/logs/conn.log");
naslog_conn_create_tbl(0, 0);
}
else
{
++v6;
}
if ( sqlite3_open("/etc/logs/conn.log", &v27) )
return 4294967276LL;
}
// ...

但是这个函数拼接 SQL 语句时使用了预编译,无法进行 SQL 注入,因此漏洞点可能不在这一条路径上面。

回到 log_tool 中,当没有传入 -S 参数时,代码会在 [4] 处调用 naslog_conn_add2,这个函数也位于 libuLinux_naslog.so.2.0.0

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
// ...
v14 = sqlite3_mprintf(
"INSERT INTO NASLOG_CONN \t( conn_type, conn_user, conn_ip, conn_comp, conn_res, conn_serv, conn_action, conn_a"
"pp, conn_action_result, conn_client_id, conn_client_app, conn_client_agent ) \tVALUES \t( %d, '%s', '%s', '%s'"
", '%s', %d, %d, '%s', %d, %Q, %Q, %Q);",
v11,
v1,
a1 + 129,
a1 + 194,
v78,
v12,
v13,
v56,
v57,
v58,
v59,
v60);
if ( sqlite3_open("/etc/logs/conn.log", &v65) )
{
sqlite3_free(v14);
return 4294967276LL;
}
else
{
sqlite3_busy_timeout(v65, 20000LL);
LODWORD(modes) = sqlite3_exec(v65, v14, 0LL, 0LL, 0LL);
// ...
}
// ...

那么很明显,函数在拼接 SQL 语句时使用了不安全的 %s 参数,是存在 SQL 注入风险的。其中 INSERT 语句的 conn_app 列就是外部传入的 -A 参数值。

到这里我们就找到了公告中提到的两个漏洞点,参数注入导致 SQL 注入,但是还存在一些关键问题需要解决。

  1. 执行 log_tool 命令时默认是带有 -S 参数的,导致无法调用到 naslog_conn_add2 函数,不能触发 SQL 注入。
  2. 假设可以触发 SQL 注入,考虑到目标数据库是 sqlite,利用思路应该是写 web 后门。那么这个后门应该写入到系统哪个位置才能触发。

第一个问题,观察 sub_A880 拼接命令的代码,使用了 snprintf 函数,最大长度为 0x800。那么第一个思路是能否通过传入超长的用户名或主机名,让格式化字符串中的 -S 参数被截断。查看 Samba 源代码发现,用户名最大长度为 0x200,主机名最大长度为 256,因此无法截断参数。

另一个思路就是从命令本身出发,能否传入某些字符将后续的参数屏蔽。我想到使用 # 字符来注释后续参数,形成类似如下的命令

1
/usr/local/samba/sbin/log_ratelimit.sh -t 1 -u 'test' -A "sql_payload" -p '127.0.0.1' -m 'linux' -i 1 -n 9 -a '---' # 'dummy' -p '192.168.x.x' -m 'xxx' -i 1 -n 9 -a '---' -S

escape_shell_string 会在 # 前面也添加反斜杠转义,导致注释失败。

smbrun 最终是调用 sh -c 执行命令的,所以可以考虑一些 SHELL 中特性,当在一条命令中使用 --,即两个短横线时,表示命令选项结束,后续的内容会被认为是普通数据。所以我们可以构造这样的命令

1
/usr/local/samba/sbin/log_ratelimit.sh -t 1 -u 'test' -A "sql_payload" -p '127.0.0.1' -m 'linux' -i 1 -n 9 -a '---' -- 'dummy' -p '192.168.x.x' -m 'xxx' -i 1 -n 9 -a '---' -S

这样从 dummy 开始的内容都不会被认为是命令选项,就可以屏蔽 -S 参数,最终使 log_tool 调用 naslog_conn_add2 函数。

后续的利用思路就很简单了,在 -A 之后插入 SQL 注入的 payload,可以写入 PHP 后门文件。那么应该将后门放在哪里才能解析?

本博客曾经复现过 QNAP 的一个 SQL 注入漏洞,当时提供的思路为结合 MusicStation 或 VideoStation 等 PHP 编写的插件来利用。实际上在系统本身的 Web 服务中,某些路径下也可以解析 PHP 代码。例如 /mnt/ext/opt/QuLog/opt/www/,在该目录下放置一个 PHP 文件,访问即可执行且不需要进行身份验证。

那么简单总结一下该漏洞的利用条件和思路

  1. 首先需要能够访问到系统的 SMB 以及 Web 服务,并且这个 SMB 服务应该是 SMB Service 插件启动的
  2. 利用参数注入漏洞,通过用户名向命令中插入 -A 以及 --,在 -A 参数中布置 SQL 注入 payload
  3. 触发 SQL 注入漏洞,向 /mnt/ext/opt/QuLog/opt/www/ 目录下写入了 PHP 后门文件
  4. 访问后门实现任意命令执行

CVE-2024-50388

根据官方公告,该漏洞影响 QNAP NAS 中的 HBS3 插件,这个插件是用来将本地和云端数据进行同步的,也是 QNAP NAS 中使用非常广泛的一个插件。根据 ZDI 在比赛中发布的信息,可以知道这是一个命令注入漏洞。

依旧是下载新版插件进行补丁对比,HBS3 的大部分组件都是用 Python 编写的,而新版本中这些文件被编译成 python3.11 版本的字节码,目前似乎还没有能够完全反编译的工具。不过通过分析,该漏洞可能并不是位于 python 代码中,对比发现新版的 rsync 等程序也发生了变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Old
sprintf(v22, "%s %s \"%s\" %s \"%s\"", "/sbin/rsync_util", "-u", v19, "-s", v20);
v14 = popen(v22, "r");
v15 = v14;
if ( v14 )
{
if ( fgets(v21, 64, v14) )
{
v16 = strchr(v21, 10);
if ( v16 )
*v16 = 0;
v17 = strtol(v21, 0LL, 10);
}
else
{
v17 = -6;
}
pclose(v15);
}
else
{
return -6;
}
return v17;
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
// New
sprintf(s, "%s %s \"%s\" %s \"%s\"", "/sbin/rsync_util", "-u", en_username, "-p", password);
result = mkstemp(templatea);
if ( result != -1 )
{
close(result);
v3 = 0;
sub_42C100(templatea, 0LL, 0LL, "/sbin/rsync_util", "-u", en_username, "-p");
v4 = fopen(templatea, "r");
if ( v4 )
{
while ( !feof(v4) )
{
while ( fgets(nptr, 65, v4) )
{
v5 = strchr(nptr, 10);
if ( v5 )
*v5 = 0;
v3 = strtol(nptr, 0LL, 10);
if ( feof(v4) )
goto LABEL_8;
}
}
LABEL_8:
fclose(v4);
}
unlink(templatea);
return v3;
}
return result;

很明显,旧版代码拼接命令之后直接使用 popen 来执行,而新版改为使用 sub_42C100 函数,其内部实际上是调用 execve 参数化执行,避免能够直接注入命令。

rsync 这个服务默认情况下是关闭的,需要手动在 HBS3 插件中开启,开启之后程序会监听 873 端口。

下面需要简单了解一下 rsync,弄清楚在什么情况下可以触发到这个命令注入。rsync 是一种 Linux 下的远程数据同步工具,可以快速在两台主机之间同步文件,能够用来实现文件备份等操作,这种服务可能在企业场景下会有更多使用。

它在服务端和客户端之间使用 “rsync 算法” 来同步文件,由于某些原因,两端通信使用的协议并没有被标准化,而且实现了 rsync 的开源项目也比较少,可以参考 samba 提供的文档以及抓包来大致了解协议结构。

首先我们分析一下漏洞点,它是在 auth_server 函数中被调用的

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
// ...
if ( RConf_Get_Field("/etc/config/rsyncd.conf", 4705734LL, "HBS3 AuthMode", auth_mode, 64LL) )
sub_45AB70(auth_mode, 64LL, 0x472BE2LL, "RSYNC");
if ( !strcmp("RSYNC", auth_mode) || !strcmp("RSYNC_AND_NAS", auth_mode) )
{
v55 = qword_6A5DC0;
memset(dest, 0, sizeof(dest));
if ( !qword_6A5DC0 )
v55 = "/etc/config/rsyncd.conf";
strncpy(dest, v55, 0x1000uLL);
if ( RConf_Get_Field(dest, 4705734LL, "rsync user", rsync_user, 65LL) )
strcpy(rsync_user, "rsync");
if ( RConf_Get_Field(dest, 4705734LL, "rsync enpswd", rsync_encpwd, 513LL) )
{
if ( RConf_Get_Field(dest, 4705734LL, "rsync pswd", rsync_pwd, 129LL) )
{
strncpy(src, "rsync", 0x81uLL);
}
else
{
sprintf(command, "/sbin/get_encstr %s e 2>/dev/null", rsync_pwd);
v56 = popen(command, "r");
v57 = v56;
if ( v56 )
{
fgets(rsync_encpwd, 513, v56);
pclose(v57);
}
rsync_encpwd[strlen(rsync_encpwd)] = 0;
sub_42B690(dest, 4705734LL, "rsync enpswd", rsync_encpwd);
sub_42B740(dest, 4705734LL, "rsync pswd");
v58 = Decrypt_Rsync_Password(rsync_encpwd, src, 129LL);
v59 = 342;
if ( v58 < 0 )
goto LABEL_77;
}
}
else
{
v58 = Decrypt_Rsync_Password(rsync_encpwd, src, 129LL);
if ( v58 < 0 )
{
v59 = 349;
LABEL_77:
sub_429040(3LL, "decrypt password fail (%d), L%d\n", v58, v59);
return __strdup(username);
}
}
*v97 = 0LL;
v98 = 0LL;
v99 = 0LL;
v100 = 0LL;
memset(v106, 0, sizeof(v106));
v73 = strcpy(v106, src);
sub_456A90(v73, v96, v97);
if ( !strcmp(rsync_user, username) && !strcmp(v97, password) )
{
sub_42C840(0, a5, 0LL, a7, a8, a9, a10, v53, v54, a13, a14);
return __strdup(username);
}
if ( auth_mode[0] != 'N' )
goto LABEL_109;
}
else if ( auth_mode[0] != 'N' )
{
goto LABEL_110;
}
if ( auth_mode[1] == 'A' && auth_mode[2] == 'S' && !auth_mode[3] )
goto nas_auth;
LABEL_109:
if ( !strcmp("RSYNC_AND_NAS", auth_mode) )
{
nas_auth:
v75 = dword_6A6074;
v74 = dword_6A5EC4;
LABEL_112:
v76 = sub_4105C0();
sub_429040(6LL, "%s:L%d [%s] check nas account, d[%d] s[%d]\n", "auth_server", 374, v76, v74, v75);
if ( (a3 != -1 || dword_6A6070 || dword_6A6074 != 1 || dword_6A5EC4 != -1 || dword_694F1C != 1 || dword_6A6078)
&& (check_nas_pwd(username, password) || sub_42B900(username) != 1) )
{
sub_42C840(1, a5, username, a7, a8, a9, a10, v77, v78, a13, a14);
return 0LL;
}
// ...
}
// ...

根据函数名称以及部分代码推断,进行 rsync 身份验证时可能会调用到漏洞函数 (check_nas_pwd),而且传入的参数为用户名和密码。

通过抓包和静态分析代码,客户端在连接 rsync 服务端进行身份验证时,可能会进行以下几个步骤

  1. 服务端发送 @RSYNCD,表明自己的版本,以及支持的身份验证 hash 算法
  2. 客户端发送 @RSYNCD,表明自己的版本,选定一个 hash 算法
  3. 服务器发送 AUTHREQD,要求密码认证
  4. 客户端将账户信息发送到服务端

因此可以推断,客户端发送用户名和密码时,就会调用到漏洞函数,如果在用户名中直接构造命令注入 payload,就能实现任意命令执行。所以利用该漏洞的条件和思路应该是

  1. 设备开启了 rsync 服务,能够访问到 873 端口
  2. 向服务器发送认证信息,在用户名处构造命令注入 payload

IoC

利用这些漏洞可能会在系统中留下日志信息,我在这里做一些简单的记录。

利用 CVE-2024-50387 SMB 漏洞时,如果在 QuLog Center 系统日志中心开启了 SMB 日志记录,可能会看到如下日志信息

1
警告  2024-11-20  18:00:00  WORKGROUP\test  192.168.0.100  linux  ---  ---  SMB  ---  登录失败

或者查看 /var/nc.log 是否存在 SMB 登录失败等信息。不过考虑到攻击者可以注入任意参数,或者在获取系统权限之后擦除日志,所以以上仅能作为简单的参考,还可以排查 WEB 路径下是否多出了未知的 PHP 文件。

系统日志中心似乎并不记录和 rsync 相关的日志,因此排查 CVE-2024-50388 的利用可以关注设备是否外联了异常 IP 地址,系统是否存在可疑文件等。

值得一提的是,两个漏洞分别影响 SMB 和 rsync 服务,这些服务可能不会开放在公网环境,受影响的插件版本也有限。此外,QNAP 在漏洞被发现后迅速采取了行动,推出新版本进行修复。而且,当 NAS 连接网络时,还会自动下载 Hot Patch 作为临时缓解方案。在这些努力下,这些漏洞被利用的可能性较低,但依然建议用户将相关组件升级到最新版,确保安全。

参考链接

https://unix.stackexchange.com/questions/11376/what-does-double-dash-double-hyphen-mean

https://www.qnap.com/en/security-advisory/qsa-24-41

https://www.qnap.com/en/security-advisory/qsa-24-42

https://www.bleepingcomputer.com/news/security/qnap-patches-second-zero-day-exploited-at-pwn2own-to-get-root/

https://rsync.samba.org/how-rsync-works.html

  • Title: CVE-2024-50387 & CVE-2024-50388
  • Author: Catalpa
  • Created at : 2024-11-21 00:00:00
  • Updated at : 2024-11-21 13:47:07
  • Link: https://wzt.ac.cn/2024/11/21/QNAP_PWN2OWN_2024/
  • License: This work is licensed under CC BY-SA 4.0.
On this page
CVE-2024-50387 & CVE-2024-50388