CVE-2022-1040 Sophos Firewall 服務架構與認證繞過漏洞分析之旅
漏洞信息
前端時間 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` 回車進行跳轉:
