Detecting Default Meterpreter HTTPS Listeners

Meterpreter is an advanced payload within the well-known Metasploit Framework (MSF).

We will look specifically at the reverse_https payload and see how we can detect the listener in our environment. I always tell my junior analysts to make sure they can detect the low-hanging fruit. For instance, most tools such as Metasploit and Cobalt Strike are very powerful, but they all come with moderately dynamic defaults that can be fingerprinted.

In this blog post, I will show you how to set up a meterpreter HTTPS listener, capture the certificate details for fingerprinting, and then end with some ideas for detection at scale.

Setting up the HTTPS Listener

I am using a Kali Linux VM in my lab environment in these examples. Still, you can use whatever you are comfortable using, such as Parrot OS or a vanilla Linux with Metasploit and all the dependencies installed. For example, the following commands are within a msfconsole session.

msf6 > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp
msf6 exploit(multi/handler) > set PAYLOAD windows/meterpreter/reverse_https
PAYLOAD => windows/meterpreter/reverse_https
msf6 exploit(multi/handler) > set LPORT 443
LPORT => 443
msf6 exploit(multi/handler) > set LHOST demo.c2.fakelabs.org
LHOST => demo.c2.fakelabs.org
msf6 exploit(multi/handler) >

By default, the Meterpreter session will continue to reach back to you for five minutes. If it is unable to connect back after that, it will terminate. You can extend this by setting the SessionCommunicationTimeout option to your choice. Setting this option to 0 ensures that your session will reattach whenever the target comes back online, as long as the payload handler is running.

metasploit-framework
msf6 exploit(multi/handler) > set SessionCommunicationTimeout 0
SessionCommunicationTimeout => 0
msf6 exploit(multi/handler) > exploit -j
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.
msf6 exploit(multi/handler) >
[*] Started HTTPS reverse handler on https://demo.c2.fakelabs.org:443

Querying the TLS Certificate

Once we have the listener configured, we can interact with it using the Nmap Scripting Engine (NSE) to extract the certificate details that were generated automatically. You can omit the port, and Nmap will scan the default range, or you can choose to scan all 65k ports. For this example, I will only scan the default TLS port.

~> nmap -sV --script ssl-cert,ssl-enum-ciphers -p 443 demo.c2.fakelabs.org

In the image above, notice that the Subject and the Issuer are the same. Additionally, there are deprecated TLS versions in the cipher suite list. We will come back to this later when we discuss fingerprinting.

HTTPS Inspection

The meterpreter HTTPS listener uses a default response body when it receives a connection that is NOT from a payload. To observe the HTTP response body, we can use curl with the options to ignore self-signed certificates, show the HTTP response headers, and increase verbosity.

~> curl -kiv https://demo.c2.fakelabs.org

Fingerprinting

Firstly, I want to mention that reliably fingerprinting TLS communications is not trivial. There has been a lot of work pioneered by Ivan Ristic and JA3/JA3S. However, I want to show how we can use observations from source code review, network traffic analysis, and server interrogation to fingerprint default meterpreter HTTPS listeners.

In my lab, I noticed that in the zeek x509.log, the certificate key length, algorithm, and key size are always constant for all communications involving the default HTTPS meterpreter listener.

~> zeek-cut certificate.key_length  certificate.key_type  certificate.sig_alg < x509.log
2048 rsa sha256WithRSAEncryption

The source code below corroborates much of my findings. However, remember that this method is only valid if the default options are not changed or a future update doesn’t modify the source code. Since we are only looking at default deployments, what you will have to do as a defender is monitor the code for changes and adjust accordingly.

When we aggregate all the information observed, a list similar to the following is a great starting point for a fingerprint.

Subject & Issuer are identical, but new values are generated for each listener.
Deprecated TLS versions
Compressors: NULL
Certificate key length: 2048
Certificate type: rsa
cipher preference: client
TLS Server Signature Algorithm: sha256WithRSAEncryption
HTML Body: <html><body><h1>It works!</h1></body></html>

The essential data point that minimizes false positives is the HTTPS response body. When we combine, for instance, the same Subject and Issuer with the HTTPS response body, the confidence in a positive finding increases. So, if you are wondering if a defender can find success by only querying for that response body, I would say, sure, that could work in small environments. However, success may be limited as you scale up to large enterprise networks.

Detection

There are several approaches to look at when hunting default meterpreter HTTPS listeners. I will discuss three methods. The first is a simple python script that will connect to a target host and check the TLS certificate subject and issuer. If they are the same, it will check the server response body and report if it found a possible meterpreter listener.

#!/usr/bin/python3
from socket import socket
import ssl
import M2Crypto
import requests
from bs4 import BeautifulSoup as bs4
from urllib3.exceptions import InsecureRequestWarning

# Suppress only the single warning from urllib3 needed.
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)


def main():
    host = 'demo.c2.fakelabs.org'
    port = 443
    default_body = 'it works!'

    cert = ssl.get_server_certificate((host, port))
    x509 = M2Crypto.X509.load_cert_string(cert)
    sub = x509.get_subject().as_text()
    iss = x509.get_issuer().as_text()

    url = 'https://' + host + ':' + str(port)

    # check if Subject and Issuer are the same
    if sub == iss:
        r = requests.get(url, verify=False)
        soup = bs4(r.content, "html.parser")
        html_body =  soup.body
        # check if we find a default response body
        if html_body.h1.text.lower() == default_body:
            print(f"Host: {host} on port: {port}")
            print("Found possible meterpreter reverse_https listener")



if __name__ == '__main__':
    main()

The python script above will need some work to scale beyond checking one host, but that should give you a starting point. I have the script in a git repo where pull requests are welcome.

Next up is what I call a quick win, assuming you have the relevant datasets. For example, suppose you collect TLS certificate data and aggregate it into Splunk, Elastic, or some other SIEM. In that case, you can search for instances where the certificate issuer and subject are the same and then use that as a pivot point to validate other items in your fingerprint. I would even include an additional filter using JA3 if your team or organization is mature enough to use them.

Last but not least, one of my favorite network security tools is zeek. We can write a zeek script to look at TLS certificate metadata and raise an alert based on our criteria. The zeek script below is not optimized for general network use but is a great starting point. You can test it by running it against a PCAP with known default meterpreter reverse_https activity. After that, you should see a notice.log generated with the details that will allow you to identify the associated source and destination hosts you will need to investigate.

@load base/files/x509
@load base/frameworks/notice

module MSF_TLS;

export {
    redef enum Notice::Type += {
        Metasploit_TLS_Cert,
    };
}
## Generated for encountered X509 certificates.
event x509_certificate(f: fa_file , cert_ref: opaque of x509 , cert:X509::Certificate )
{
    ## check if subject is empty
    if ( ! cert?$subject ) {
        return;
    }
    ## check if issuer is empty
    if ( ! cert?$issuer ) {
        return;
    }
    ## we only want to process matching subject and issuer
    if ( cert$subject != cert$issuer ) {
        return;
    }
    ## If subject and issuer match then check signature algorithm
    ## key lenth and type according to the derived fingerprint
    if ( cert$key_type == "rsa" )
    if ( cert$key_length == 2048 )
    if ( cert$sig_alg == "sha256WithRSAEncryption" )

    local conn: connection;
    for (cid in f$conns)
        conn = f$conns[cid];

    NOTICE([$note=Metasploit_TLS_Cert,
            $msg=fmt("Potential Metasploit TLS Cert, '%s'", cert$issuer),
            $conn=conn,
            $sub=cert$subject]);
}

Connecting to the reverse_https listener using a payload executed on a target system is outside the scope of this blog post. However, I will include a link to the documentation recipe for your reference.

That is all for now, thanks for reading.

Start a discussion or ask a question.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: