本文为 Array Networks vxAG 远程代码执行漏洞分析的第二部分,主要介绍设备 License 和 VPN 安全问题。
License
正常导入设备后默认处于试用状态,只能使用部分基本功能,VPN 等功能需要导入合适的 License 之后才能开启。
(未导入 License 时,支持的功能为空)
(无法使用 VPN 功能)
正常来讲,我们需要获取试用版 License,或者分析验证逻辑,构造出合适的 License 来解锁各项功能,但是对于这款设备来说,有一种更方便的方法。
在某平台的设备帮助手册中,我找到了如下截图信息:
文档中介绍了如何部署设备以及激活 License,其中的截图包含一个已经过期的具有 VPN 模块授权的 License (J1ErSzPo-3TiY8OYU-bTEsUFdn-wfA=#131-4d67d9ab-a9cf1726-4c67eaa3-beef0123-4d#7ebaa-9cfe1#dc-ba98765),到期时间为 2016 年 5 月 9 日。考虑能否直接将这个 License 导入设备中呢?
尝试修改时间之后导入 License,设备会报 License 非法错误:
猜测每个 License 对应一个设备的序列号,截图中对应的序列号为:4B756D5412DC3000000605120655416,想要导入 License 的话可以考虑修改设备序列号,或者简单分析一下 License 验证逻辑。
首先定位到验证逻辑的入口点:class.cliWrap_gLicense.php
1 | case ($this->classId . '_gLicenseImportValidate'): |
代码会调用 system license CLI 命令验证 License 内容。全局搜索之前的报错信息,发现其出现在 /ca/bin/backend 二进制中。
简单分析逻辑,set_license 函数负责检查和设置 License,其中会调用 decode_actkey 函数来解码 License 内容。
1 | int __fastcall decode_actkey(const char *licensekey, feactl_t *feactl_p, int show_flag, int *days) |
观察到关键的验证函数是 verify_license_hash,如果它的返回值不为 1,则表示验证失败。我们采取简单的破解方法,将此处判断取反,保存 patch 后覆盖到系统文件中。重新启动设备,先执行命令 date 200605261145
修改掉系统时间,然后再导入 License:
这样子就成功激活了 VPN 等功能。
VPN 配置
参考以下流程来配置 VPN 功能
添加虚拟站点
Virtual Sites -> Add
各项配置根据实际情况填写,注意 IP 地址和端口的配置,点击保存。
添加账户
双击刚刚添加的虚拟站点条目,切换到虚拟站点菜单
Local Accounts -> Add
填写用户名和密码即可。
执行以上操作后访问之前配置的端口即可看到 VPN 界面:
虽然还缺少一些配置信息,但足以用于展开各项测试。
漏洞分析
本文要介绍存在于 VPN 功能中的一个远程代码执行漏洞,在当前版本(9.4.0.5)测试成功,在较新版本中似乎也存在,最新版本中应该已经被修复。
通过检索响应头等信息,发现 uproxy 程序,此程序是类似 nginx 的代理服务器,其内部会处理一些请求,另外一部分请求会被转发到其他服务中。
stage_classify_url 函数是请求分发的入口点,先调用 sec_content_search 函数,尝试匹配 URI,最终实现函数为 patricia_insearch
1 | v19 = patricia_insearch(content_tree, url_host_buf, v13 - 1, 2u); |
这个函数将用户发送的 URI 和 content_tree 全局结构体中保存的进行比较,通过交叉引用找到 content_tree 结构体初始化位置
1 | sysctlbyname("net.inet.clicktcp.content_tree_ptr", &content_tree, &v302, 0LL, 0LL) |
这里本质上是读取内核中的配置信息,我们可以在命令行中读取到对应内容
1 | # /sbin/sysctl -a | grep content_tree |
该结构位于内核某个地址,在当前版本中,这个结构保存了很多条路由信息,每个路由对应编号和 flag,后续代码的 switch 结构会根据编号对路由进行处理。当 flag 等于 13 时表示此路由为 public request,请求会被转发到监听于 127.0.0.1:80 的 lighttpd web 服务中。
当请求 URI 不属于这 211 条路由时,stage_classify_url 还有一些特殊处理
1 | // ... |
逐步分析各个 URI,可以找到一个叫做 /client_sec 的接口,这是一个 public request,请求会被直接转发到 lighttpd 中。
通过 ps 看到 lighttpd 启动命令,定位到配置文件:/ca/fileshare/httpd.conf
1 | server.modules = ( |
发送 /client_sec 请求时可以访问到位于 /ca/fileshare/htdocs/client_sec 下的文件,例如发送以下请求:
/client_sec 目录下有一些资源文件,但重点不在这里。我们考虑既然可以访问 /ca/fileshare/htdocs/client_sec 下的文件,那么能否通过路径穿越等手段访问到位于 /ca/fileshare/htdocs 下的文件呢?因为 /ca/fileshare/htdocs 目录下存放了一些 perl 脚本文件,结合 lighttpd 开启了 cgi 支持,如果可以访问这些文件的话就可以调用其中的功能。
我们构造请求测试:
通过在 URL 中添加路径穿越字符,确实可以访问到上一层的文件。(直接访问 /prx/000/http/localhost/acl.pm 会由于代理服务器返回 302)
不过以 .pm 结尾的文件服务器并不会执行,而是直接返回其内容,尝试访问无拓展名的脚本如 addfolder:
这样脚本中的内容就被执行了,我们可以分析这些 perl 代码中是否存在问题。下面列举一个漏洞:
在 addfolder 中存在这样的代码
1 | $kvars{'uid'} = -1; |
read_kernel_parameters 函数的定义:
1 | my $fileshare_header = "HTTP_X_AN_FILESHARE"; |
我们发现 kvars 参数中的内容实际上是用户请求头 X_AN_FILESHARE 中的各项参数,代码中用注释列举了可能的 X_AN_FILESHARE 请求头格式。
回到 addfolder,当请求类型不为 POST 时,会调用 fileshare::displayInfo 函数
1 | sub displayInfo |
代码会使用 is_login_failure 函数鉴权,无论是否通过,后面都会调用 read_template 函数尝试读取一个 HTML 模板文件,这个函数的参数是用户可控的。
1 | sub read_template |
我们看到代码会使用第四个参数即 fshare_template 拼接一个路径,然后打开对应的文件读取并使用其中内容,期间缺少对数据的过滤,通过构造路径穿越 payload 可以读取一些敏感文件。
类似的“小问题”还有很多,限于篇幅我们只介绍其中一个。除这些小问题之外,我们希望有一种办法能实现远程代码执行。
参考perl 代码审计文章,在 perl 中除常规的 system、exec 之外,open 函数也可以执行系统命令,只需要在路径的最后添加一个 |
字符。在设备的 perl 代码库中查询相关风险点,似乎只有 open 有利用机会。
查找引用 open 函数的位置,发现名为 printFile 的函数
1 | sub printFile |
第 10 行调用了 open 函数去打开外部传递的参数 $fullname,查看使用 printFile 的位置可以定位到位于 cifs 文件中的 downloadSmbFile 函数。
1 | sub downloadSmbFile |
简单来说,这个函数负责从远程 SMB 服务器上下载文件到本地,用户可以提供服务器的地址、文件路径、用户名和密码等参数,最后代码会调用 secureBackticks 函数以数组方式执行 smbclient 程序,完成下载文件操作。这里可以控制 smbclient 的各项参数,但是并不能直接获取 shell。
在调用 printFile 函数的位置会用 $tmpdir 和 $file 两个参数拼接一个路径,其中 $file 参数即 $path 是用户可控的。看起来只需要控制 $path 参数即可实现命令执行,但是仔细观察代码逻辑发现并没有这么简单。只有当 $error 为空,即从 smb 服务器取回文件时没有出错才能执行 printFile 函数。
最初我想到先在远程服务器上搭建一个 SMB 服务器,在服务器中放一个文件名带有 |
字符的文件,然后正常构造各个参数让 smbclient 取回这个文件,不过 SMB 服务不能支持文件名带有 |
的文件。
例如以下测试:
这个包含非法字符的文件会被自动重命名。
那么这个位置就无法利用了吗?我们仔细观察代码逻辑,在判断命令是否执行成功时调用了函数 getSmbConnInfo 以及 getSmbCmdInfo
1 | sub getSmbConnInfo |
这些函数尝试在命令的输出中匹配一些字符串,如果匹配成功则返回成功。
首先按照代码中的命令构造一条,在设备的终端执行:
随意构造的服务器地址导致返回无法连接错误信息。我们看到 smbclient 带有一个 -M 选项,尝试添加此选项:
添加选项之后打印了我们控制的信息。合理利用各个参数来排布这些错误信息,结合代码判断命令成功的方式,就可以绕过这些判断函数,从而执行到 printFile。这样 path 参数就完全可控,不受非法字符限制,我们只需要构造命令注入 payload,并在其末尾添加一个 |
字符即可实现代码执行。
权限提升
通过上面的命令执行只能以 nobody 权限执行命令,需要考虑如何提权。
提权首先要考虑使用系统中自带的,具有 SUID 权限的二进制程序,通过筛选此类文件,可以找到一个名为 webui_localdb_file 的程序。
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
我们看到程序首先调用 seteuid 和 setegid 设置权限到 root,然后根据外部传入的参数执行各种命令,显然存在命令注入,利用此程序即可实现权限提升。
演示
请观看下面的演示视频:
本文介绍了关于 ArrayNetworks vxAG 设备 License 破解和 SSLVPN 远程代码执行的相关内容,文中还有一些细节需要解决,另外在新版中 webui_localdb_file 对输入的参数进行了过滤。如何在新版中实现提权?如何解决代码执行中的一些小问题?我们就留给读者去探索吧。
- 本文作者: CataLpa
- 本文链接: https://wzt.ac.cn/2022/12/20/ArrayVPN_rce2/
-
版权声明:
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。