漏洞信息

前端时间 Sophos Firewall 爆出了一个认证绕过漏洞 CVE-2022-1040 ,最近在深入分析 Sophos 服务架构的同时,完整复现了该漏洞。主要是在 `User Portal` 及 `Webadmin` 两个接口存在认证绕过漏洞,漏洞巧妙利用了 Java 和 Perl 处理解析 JSON 数据的差异性,实现了变量覆盖,从而导致认证绕过及命令执行。漏洞适用范围为 Sophos Firewall v18.5 MR3 及以下版本。

环境搭建

首先从官网下载老版本虚拟机。本文研究下载版本为 `VI-18.5.2_MR-2.VMW-380`:

按照提示很容易完成安装。Sophos 的 Web 接口主要通过 Java 实现,由启动命令可以看出 Sophos 使用的是 openjdk 环境,如果在 Java 启动参数中直接添加调试参数,将会出现找不到 `libjdwp.so` 动态链接库的错误:

可以通过自行上传完整 JDK 来解决这个问题:

重启 Java 服务即可看到监听的端口。

服务架构

Sophos 中的服务架构不是很复杂,主要使用了 Apache 、 Jetty 等 web 服务,第一层语言为 Java 通过网络通信的方式与后端 Perl 服务进行交互:

0x01 Apache 配置

Apache 的启动命令如下:


apache -d /_conf/httpd -DFOREGROUND

进入 `/_conf/httpd` 目录,存放有 Apache 的配置文件,通过分析 `httpd.conf`,了解 Apache 引用了 `/cfs/web/apache/httpd.conf` 配置:


Define userportal_listen_port 65004Define webconsole_https_port 65003Define SSLCertificateFileWithPath "/conf/certificate/ApplianceCertificate.pem"Define SSLCertificateKeyFileWithPath "/conf/certificate/private/ApplianceCertificate.key"Define https_cert_valid true

从配置中可以看出 Apache 开放了两个主要的端口 `userportal 65004` 和 `webconsole 65003` :

在 Apache 配置目录下搜索 `ProxyPass`,找到的代理转发配置如下:


./ssl.conf:53:    ProxyPass /webconsole/images !./ssl.conf:54:    ProxyPass /webconsole/css !./ssl.conf:55:    ProxyPass /webconsole/javascript !./ssl.conf:56:    ProxyPass /webconsole http://localhost:8009/webconsole./ssl.conf:57:    ProxyPassReverse /webconsole http://localhost:8009/webconsole./userportal-static.conf:64:    ProxyPass /userportal/images !./userportal-static.conf:65:    ProxyPass /userportal/CRSSL !./userportal-static.conf:66:    ProxyPass /userportal http://localhost:8009/userportal./userportal-static.conf:67:    ProxyPassReverse /userportal http://localhost:8009/userportal

代理转发策略将 `webconsole` 和 `userportal` 端口分别代理到 `8009` 端口的不同 URL :


ProxyPass /webconsole http://localhost:8009/webconsoleProxyPass /userportal http://localhost:8009/userportal

0x02 Jetty 配置

Jetty 配置文件为 `/usr/share/jetty/start.ini`,开启了本地服务的 `8009` 端口:

Jetty 的启动参数在 `/usr/bin/jetty` 脚本中配置,如果要修改可直接修改该文件最后 Java 执行部分。

0x03 CSC 配置

CSC 是 Sophos 的主要服务之一,主要负责启动各个服务进程及提供 API 接口供其他程序服务调用。其启动命令为:


csc -L 3 -w -c /_conf/cscconf.bin


CSC 为标准的 ELF 32bit 可执行程序,可通过逆向分析其中功能。`cscconf.bin` 中在 CSC 程序中有调用解压,猜测是一个加密压缩包。

`/usr/bin/csc` 由 C 语言编写,负责启动加载其他的服务以及加载 Perl 代码,在虚拟机中 CSC 启动部分服务如下:

程序中的 `extract_conf` 函数负责解密 `cscconf.bin` 并提取压缩包中的内容:

`decrypt_bin` 函数主要是通过异或算法将 `cscconf.bin` 数据解密为 `cscconf.tar.gz` 压缩包格式:

每次取 `0x420` 个字节通过 `xor_decrypt` 函数进行加密块解异或解密,将解密后数据中的 `0x400` 个字节写入 `tar.gz` 文件:

`xor_decrypt` 核心代码如下,主要通过与 `0x80DCB40` 地址中实现存放的 64 字节逐一进行异或处理:

通过逆向该算法,使用 Python 编写出加解密算法实现代码。解密得到 `cscconf.tar.gz` 压缩包,解开压缩包目录如下:

在压缩包中的其中一个目录名为 `service` ,推测 CSC 通过该目录下的配置文件启动相关服务:

补丁对比

配置 Sophos vmware 网卡连网后等待一段时间,将虚拟机上的文件与原来的文件进行对比,其中有两个修改的地方,一处为 `web.xml`,另一处添加了 `RequestCheckFilter.class` 文件:

web.xml` 增加了一段配置,主要给 Sophos Java 代码添加 `RequestCheckFilter `过滤器,过滤器主要检测 request 请求包中的 JSON 参数是否包含不可见字符:

检测规则中,JSON 参数的每个字符都必须是 `32~127` 之间,如果超出范围则会跳转到登录界面:

那么给我们的启发就是此次漏洞和 JSON 参数中的不可见字符有着直接的关系,应该是字符编码导致的认证绕过。

JSON 解析差异性分析

漏洞原理可简单理解为 Java 在使用 `unicode \u0000` 时,JSON 认为 `key` 是两个不同的 `key` 并没有 `0` 字节截断,当 Java 把含有 unicode 编码的 `key` 发送给后端的 Perl处理时 `\u0000` 产生了截断效果,使得带有 unicode 编码的 `key` 变为了 `mode`, Perl 可以处理重复 `key` 的 JSON,如果重复则后面覆盖前面的值:

我们可以通过一个示例来对比不同语言处理 JSON 重复键的差异性。Java 处理带有 unicode 编码的 `key` 时可以正常解析:


import org.json.JSONObject;import org.json.JSONException;import java.io.*;class test {    public static void main(String[] args) {        try{          System.out.println(new JSONObject("{ \"name\": \"test\", \"name\\u0000ef\": \"test2\"}"));        }catch (JSONException e){          System.out.println(e);        }    }}

Perl 处理 Java 传递过来的零字节字符串就会产生截断效果,在处理相同 `key` 值的 JSON 时会取最后一个 `key` 对应的 `value` :


#!/usr/bin/perluse JSON;
my %rec_hash = ('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'a' => 6);my $json = encode_json \%rec_hash;print "$json";

其结果为 `a` 被覆盖为了最后一个 `a` 的值:

不同语言对 JSON 解析的差异性是此次认证绕过漏洞的核心原理。Java 接收来自用户的以下数据(其中 `\u0000` 后面的 `ef` 是为了让 JSON中 `key` 的 hash 排序将 `716` 排到 `151` 的后面):


{"mode":151,"mode\u0000ef":716}

登录认证分析

通过登录 `webadmin` 获取登录数据包:

根据 `/webconsole/Controller` 及 `mode=151` 寻找 Java 代码中的处理逻辑,首先分析 `web.xml` 中路由对应的 Servlet :

主要由 `CyberoamCommonServlet` 进行处理,该类主要解析 `mode` 值,并通过 `mode` 进行分发:

进入 `_doPost` 函数继续进行分发, `EventBean` 从数据库中获取 `mode` 对应的属性,其中就包括关键属性 `Requesttype` :

通过 `Requesttype` 将请求分为了三种:

  •  `Requesttype` 为 `1` ,使用 `CyberoamAjaxHelper` 处理;
  •  `Requesttype` 为 `2` ,使用 `CyberoamCustomHelper` 处理;
  •  `Requesttype` 为其他值时,使用 `generateAndSendOpcode` 处理。

进入 `CyberoamCustomHelper` 之后,通过匹配 `mode` 值进行分发,将会进入 `WebAdminAuth` 类中进行处理:

`process` 函数对请求中的 JSON 字段进行解析,获取 `jsonObject` 之后将会给 `cscClient` 通过 `localhost:299` 发送给 CSC 进程进行处理:

比较有意思的是 Sophos 通过 `cscClient` 返回的 Status code 判断是否登录成功,因此在 Java 代码中是看不到登录认证的完整过程的,如下图所示如果返回为 `200` 则会生成合法 `sessionBean` :

合法 `sessionBean` 生成过程如下,将 `session` 中填充 `username` 、 `userid` 、 `csrftoken` 等关键信息:

因此我们只需要找到后台返回 `200` 的函数即可。

CSC Perl API 分析

因为 webadmin login mode 151 的 `Requesttype` 为 `2` ,在 `_send` 函数最后获取返回值的时候使用 `getStatusFromResponse` 进行解析:

使用 `eventBean` 判断 `Requesttype` ,因为该 `eventBean` 在数据包刚开始处理的时候就根据 `mode` 值在数据库中进行搜索匹配,中间没有修改的可能性。在如下 `else` 分支中需要获取 JSON 结果的 `status` 字段:


因此在寻找可返回 `200` 的 `mode` 值时需要考虑的是要能够同时返回 `status` 字段,通过搜索数据库找到所有 `Requesttype` 为 `2` 的 `OPCODE` ,其中有 `716` 符合条件。注意 Perl 代码获取了 `request` 中的 `accessaction` 字段,并且需要该字段为 `1` 才能返回 `200` 。

漏洞复现

通过前面的分析,我们很容易构造特殊的 JSON 数据包实现认证绕过。如果数据包中返回 `status` 的为 `200` ,并且 `redirectionURL` 路由为 `index.jsp` 即为认证成功,直接取 `Set-Cookie` 中的 `JSESSIONID` 进行使用:

在未登录条件下在浏览器中添加上述认证后返回的 `session`,操作如下:

替换浏览器中的 `JSESSIONID` ,并在 url 处输入 `index.jsp` 回车进行跳转: