Tomcat
Apache Tomcat

Apache Tomcat軟件是Java Servlet、JavaServer Pages、Java Expression Language 和Java WebSocket 技術的開源實現。Tomcat由于其自身簡單、穩定、開源等特征,在中小型系統和并發量小的場景下被普遍使用,有極大的用戶使用量。
Tomcat 口令爆破
Tomcat在默認情況下提供了一些管理后臺,不同的管理后臺提供了不同的功能,這些管理后臺使用了Basic認證的方式進行權限校驗,如果暴露在互聯網上,將存在遭到暴力破解的安全風險。
其中主要包含兩種:Manager以及Host Manager。
Manager APP
在生產環境中,為了實現部署新的Web應用程序或取消部署現有的程序而不必重啟容器、動態更新程序代碼、列出一些JVM或操作系統的屬性值、列出應用程序或會話等等基礎功能,Tomcat默認包括了一個Web應用程序,通常位于$CATALINA_BASE/webapps/manager目錄下:

并且這個應用程序默認的context path也為/manager,訪問應用的方式為/manager/html。
對于這個APP的詳細介紹,可以在官方文檔找到,以Tomcat 7.0為例,文檔的位置為:https://tomcat.apache.org/tomcat-7.0-doc/manager-howto.html
我們查看manager應用下的web.xml,發現其中配置了如下servlet-mapping:

由此我們可以知道:
- /manager/html/:提供HTML格式的管理頁面
- /manager/status/:提供服務器狀態(Server Status)頁面
- /manager/jmxproxy/:提供JMX proxy 接口
- /manager/text/:提供純文本頁面
由于Tomcat Manager提供的這些功能是需要管理人員才能使用和查看的,因此如果默認開啟的話將會存在很高的安全問題,所以Tomcat并沒有直接提供這些功能。
如果想要使用這些功能,則需要在$CATALINA_BASE/conf/tomcat-users.xml中配置相關的用戶信息,包括用戶名、密碼、用戶角色,來對使用這些功能的用戶進行身份鑒別和權限驗證。

在 manager 項目中的web.xml中我們可以看到能夠使用的這些角色:

他們對應的權限分別為:
- manager-gui:能夠訪問
/manager/html/的管理界面。 - manager-script:能夠訪問
/manager/text/以及/manager/status/界面。 - manager-jmx:能夠訪問
/manager/jmxproxy/以及/manager/status/界面。 - manager-status:能夠訪問
/manager/status/的Server Status界面。
在正確的配置了用戶身份信息和角色后,就能夠訪問相應的頁面了。以本篇作者的環境為例,演示如下:
Server Status頁面:

html管理后臺:

純文本頁面-使用指令list:

JMX Proxy頁面:

對于這幾個Servlet功能的實現如果感興趣,可以查看在org.apache.catalina.manager 目錄下的代碼,在此章節將不過多關注。

Host Manager
顧名思義,此管理后臺用于管理運行在Tomcat上的虛擬主機,文件路徑位置為$CATALINA_BASE/webapps/manager,context path為/host-manager。
詳細介紹可以在官方文檔找到,以Tomcat 7.0為例,文檔的位置為:https://tomcat.apache.org/tomcat-7.0-doc/host-manager-howto.html
servlet-mapping以及用戶權限均有兩個:

配置方式同 manager,不再重復,以下為頁面示例:
html頁面管理后臺:

純文本頁面-使用指令list:

暴力破解
剛才提到的這些應用之所以采用Basic認證進行身份驗證,原因是在應用配置、Tomcat配置等文件中進行了如下配置:
- 認證的方式(BASIC、DIGEST、FORM、SSL等)

- 通過
security-constraint配置需要鑒權的訪問路徑

- 用戶的身份信息(賬戶、密碼)、權限(角色)

- 默認的域(Realm)配置,Tomcat的
server.xml中默認配置UserDatabaseRealm,它從配置的全局資源conf/tomcat-users.xml中提取用戶信息,在Tomcat 7.0及以上版本中,還提供了LockOutRealm的組合域,用來阻止短時間內多次登錄失敗的情況。

在經過了如上配置后,再訪問這些管理后臺將需要進行Basic認證。
弱口令
雖然Tomcat沒有提供默認用戶,但是在配置文件中含有注釋了的配置樣例,其中包括的用戶名密碼:
tomcat:tomcat
both:tomcat
role1:tomcat
有的管理員可能會取消注釋直接使用這些默認配置的賬戶密碼,因此可以將其當做Tomcat的默認口令
除此之外,還有的管理員習慣于使用常用的用戶名及密碼,如:
admin:admin
admin:123456
root:root
manager:manager:
admin:admin888
...
以上僅僅是列出一小部分作為示例,實際上還有很多種類的弱口令。一旦弱口令被攻擊者猜解到,攻擊者能夠輕易的獲取一些用戶的權限,大大降低了攻擊成本。
暴破
Basic認證是一種較為簡單的HTTP認證方式,客戶端通過明文(Base64編碼格式)傳輸用戶名和密碼到服務端進行認證。
客戶端攜帶數據格式為:
Authorization: Basic dG9tY2F0OjEyMzEyMw==
服務端會根據用戶是否登陸成功返回 200 或 401 等不同的狀態碼,攻擊者可以自行組織字典進行暴力破解攻擊。
暴力破解攜帶的賬戶密碼可能是弱口令,也可能是撞庫攻擊,還可能是根據站點、管理員自身信息生成的字典等,具有更高的成功率。
如下,使用 Burpsuite的 Intruder 模塊對 Tomcat 6.0 的 /manager/html 路徑的基礎認證進行爆破:

限制
剛才提到了,在Tomcat 7以上配置文件默認添加了LockOutRealm,首先我們看一下 LockOutRealm的邏輯,代碼位于org.apache.catalina.realm.LockOutRealm。類里的字段很明了,無需解釋。

在 authenticate 方法中進行身份驗證,如果用戶登陸失敗,將調用 registerAuthFailure 方法標記用戶的登錄失敗狀態

這段代碼我貼一下:
private void registerAuthFailure(String username) {
LockOutRealm.LockRecord lockRecord = null;
synchronized(this) {
if (!this.failedUsers.containsKey(username)) {
lockRecord = new LockOutRealm.LockRecord();
this.failedUsers.put(username, lockRecord);
} else {
lockRecord = (LockOutRealm.LockRecord)this.failedUsers.get(username);
if (lockRecord.getFailures() >= this.failureCount && (System.currentTimeMillis() - lockRecord.getLastFailureTime()) / 1000L > (long)this.lockOutTime) {
lockRecord.setFailures(0);
}
}
}
lockRecord.registerFailure();
}
重點的判斷邏輯是:
如果用戶的登錄失敗次數>=5次,并且,(當前時間-上次登錄失敗的時間)>300s,將會將用戶登錄失敗的次數重新設置為0。
函數最后一行是內部類的方法,將 failures += 1,并將 lastFailureTime置為當前時間:

由此可知,在5分鐘之內同一賬戶登陸失敗5次以上,LockOutRealm 將會封鎖用戶,在未來5分鐘之內沒有新的登陸失敗的情況,會從0開始重新計數,因此這種方式是能夠一定程度緩解系統受到的暴力破解攻擊的。
Tomcat AJP 協議
AJP(Apache JServer Protocol) 協議最初是由 Gal Shachor 設計。對于Web服務器與Servlet容器通信來講,最主要目的是:
- 提高性能(主要是速度)。
- 添加對SSL的支持。
目前Tomcat中使用的版本均為AJP1.3,簡稱為ajp13。ajp13協議是面向數據包的。出于對性能的考慮,選擇了以二進制格式傳輸,而不是更易讀的純文本。
Web服務器通過TCP連接與servlet容器進行通信。為了減少昂貴的套接字(socket)創建過程,web服務器將嘗試保持與servlet容器的持久的TCP連接,并為多個請求/響應周期重復使用一個連接。
官方文檔位置:https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html
在Tomcat的server.xml 中默認配置了兩種連接器:

一種是使用的HTTP Connector,監聽8080端口,還有一個AJP Connector,監聽了8009端口。在Tomcat中這個協議的監聽的一直都是默認開啟的。
AJP Connector 的配置文檔:http://tomcat.apache.org/tomcat-6.0-doc/config/ajp.html
AJP Connector 可以通過 AJP 協議和另一個 web 容器進行交互。例如正常情況下的訪問是由客戶端通過HTTP協議訪問到Tomcat服務器返回結果,此時將由HTTP Connector處理請求,但也可以使用通過AJP協議來進行訪問,此時將由AJP Connector來處理。
但是通常情況下用戶的客戶端并不會支持AJP協議,因此想要使用AJP協議進行訪問,需要自己實現連接器,或由中間的代理服務器進行轉發。
- Apache HTTP Server 2.x 上的啟用 AJP 的 mod_proxy 模塊(在 2.2 上已成為默認配置模塊)
- 其他任何支持 JK 1.2.x 的服務器
AJP 協議配置
測試環境:
- 操作系統:CentOS 7
- Web服務器:Apache/2.4.6
- JSP服務器:Tomcat 9.0.27
- JDK:1.8.0_251
mod_jk
Mod_JK是Apache的一個模塊,其通過AJP協議實現Apache與Tomcat之間的通訊
官網地址:http://tomcat.apache.org/download-connectors.cgi
使用手冊:http://tomcat.apache.org/connectors-doc/webserver_howto/apache.html
由于配置較 mod_proxy_ajp 復雜,此處不進行演示,有興趣的朋友可以按照官方文檔自行嘗試。
mod_proxy_ajp
首先在 /conf/httpd.conf 中添加模塊:
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
在虛擬主機中設置代理轉發
<VirtualHost *:81>
ProxyPass / ajp://localhost:8009/
ProxyPassReverse / ajp://localhost:8009/
</VirtualHost>
正常啟動Tomcat,把我們命令執行的 test.jsp 放在ROOT中,使用8080端口正常以HTTP協議直接訪問項目:

使用 apache 監聽的 81 端口進行 AJP 協議轉發也可以正常訪問:

AJP協議數據包處理
在看AJP協議數據包處理之前,先來了解一下Tomcat處理一個請求的過程。大致如下流程:

一次請求的處理可以劃分為Connector及Container進行處理,經歷的過程大致如下:
- 一個TCP/IP數據包發送到目標服務器,被監聽此端口的Tomcat獲取到。
- 處理這個Socket網絡連接,使用Processor解析及包裝成request和response對象,并傳遞給下一步處理。
- Engine來處理接下來的動作,匹配虛擬主機Host、上下文Context、Mapping Table中的servlet。
- Servlet調用相應的方法(service/doGet/doPost…)進行處理,并將結果逐級返回。
而對于使用HTTP協議或AJP協議進行訪問的請求來講,在解析包裝成為request和response對象之后的流程都是一樣的,主要的區別就是對socket流量的處理以及使用Processor進行解析的過程的不同。
提供這部分功能的接口為 org.apache.coyote.Processor<S> ,主要負責請求的預處理。并通過它將請求轉發給Adapter,針對不用的協議則具有不同的實現類。
這個接口里定義了一些重要的方法:

這里主要還是針對于HTTP協議和AJP協議,抽象類AbstractProcessorLight及其子類AbstractProcessor還是對共有特性的封裝。

AbstractProcessor具有三個子類,AjpProcessor 用來處理AJP協議,Http11Processor 用來處理HTTP/1.1,StreamProcessor用來處理HTTP/2,我們先來看看針對平常使用的HTTP協議的處理。
Http11Processor 重點的process()方法,使用service()方法來處理標準HTTP請求,這里我們重點看一下:
解析請求行和請求頭部分:

在Tomcat 8.5 之后,加入了判斷是否需要HTTP協議升級:

調用prepareRequest(),將相關信息放入Http11InputBuffer對象中

然后調用Adapter將請求交給Container處理:

然后接下來是一些收尾工作。在了解了這個過程后,我們再來看一下 AjpProcessor 中service()方法,大體上是一致的流程,只是具體的細節不同,首先是一些解析數據包讀取字節的操作,這里不是重點,暫且不提,然后也是調用 prepareRequest() 方法進行預處理:

處理之后同樣的調用Adapter將請求交給Container處理

而AJP協議的任意文件讀取/任意文件包含漏洞,則出現在上面提到的 prepareRequest() 方法中。
AJP漏洞
在 AjpProcessor 的 prepareRequest() 中,惡意攻擊者可通過控制請求內容,為request對象任意的設置屬性。
在switch/case 判斷中,當attributeCode=10 時,將調用 request.setAttribute 方法存入。

所以在此攻擊者擁有了可控的點,這個點該如何利用呢?
DefaultServlet
在$CATALINA_BASE/conf/web.xml 中默認配置了如下內容:

可以看到這是一個默認的Servlet,這個 DefaultServlet 服務于全部應用,當客戶端請求不能匹配其他所有Servlet時,將由此Servlet處理,主要用來處理靜態資源。使用 serveResource() 方法提供資源文件內容:

會調用 getRelativePath() 方法獲取請求資源路徑:

這個方法存在一個判斷,如圖中紅框位置標出:如果 request.getAttribute() 中javax.servlet.include.request_uri 不為空,則會取 javax.servlet.include.path_info 和javax.servlet.include.servlet_path 的值,并進行路徑拼接,返回路徑結果。
這個結果 path 會被帶入到 getResource() 方法中返回結果,只要文件存在,即可讀取其中內容。

由此可見,配合AJP協議中的缺陷,可以控制attribute中的內容,造成任意文件讀取漏洞。
但是需要注意的是,在讀取資源文件的過程中,會調用org.apache.tomcat.util.http.RequestUtil.normalize() 方法來對路徑的合法性進行校驗,如果存在 ./ 或 ../ 則會返回 null ,在后續流程中會拋出一個非法路徑的異常終止文件讀取操作。

因此我們無法使用 ../ 跳出目錄,只能讀取Web應用目錄下的文件。
JspServlet
同樣的在$CATALINA_BASE/conf/web.xml 中,對訪問以 .jsp/*.jspx 后綴結尾的請求,調用 JspServlet 處理請求。

看一下重點的 service(),代碼如下圖,在attribute中含有如下 javax.servlet.include.servlet_path,javax.servlet.include.path_info 時,將會取出并拼接為文件路徑 jspUri。

拼接成 jspUri 后,調用 serviceJspFile() ,將此文件解析為jsp文件并執行。

因此這就構成了一個文件包含漏洞。在文件內容可控的情況下,就可以延伸為任意代碼執行漏洞,所以網上有的分析文章也出現了任意代碼執行、任意命令執行漏洞的字眼。
AJP客戶端
在了解了AJP協議的漏洞成因之后,我們只需要構造一個客戶端就可以實現自己的攻擊行為了。
AJP協議的請求和響應包結構在文檔可以看到:https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html
實現過程這里不進行描述,代碼放在如下位置,請自行觀看:
javaweb-sec/javaweb-sec-source/javasec-test/javasec-tomcat-ajp
在這里進行漏洞兩種利用方式的演示。
Web目錄任意文件讀取
任意文件讀取需要滿足的條件是:
- 訪問的地址(target)是一個沒有 Servlet 映射的地址
- request_uri 屬性不為空
- servlet_path 和 path_info 拼接得到我們想要讀取的文件
如下圖配置:

可以看到成功返回了文件內容:

JSP文件包含
假設我們在web目錄下具有可控的文件,比如我們上傳了一個aaa.jpg,文件里是一個執行whoami命令并返回結果的jsp惡意文件。

這是我們需要控制的是訪問的地址(target)是一個.jsp結尾的文件,并且 servlet_path、path_info 拼接起來是我們可控的文件路徑。

運行返回結果,可以看到我們的 jpg 文件以 jsp 解析并執行成功:

問題拓展
在實際對Tomcat AJP 漏洞的研究和利用過程中,逐漸產生了以下幾個問題,請自行思考:
- SpringBoot項目是否受到此漏洞影響?如果能受到影響,是在什么情況下?
- Struts2、Shiro、SpringMVC 等等具有一些全局過濾器的情況下,是否能夠觸發漏洞?
- jsp 作為視圖模板時是否存在漏洞?
Java Web安全
推薦文章: