NMAP SSL Data > Python > CSV

This post has a very peculiar title, but accomplished a very unique goal, the old and weird way of doing this was to export a list of issued certificate that were issued by certificate authority, this allowed for duplicate records, then it was reported into excel and reported on, this was not a very streamline and efficient way of doing such a task.

This method has a lot of drawbacks so for every certificate authority that can issue a certificate there is no correlation between issued and being bound to a service as indeed, you could only have one active certificate per service, especially if that certificate has exactly the same details in the common name (CN) - this particular way of doing it is not the way I would recommend to do it.

Instead, it makes far more sense to query all the surfers in a list or in a certain network range for all their open ports then using NMAP and the script “ssl-cert” to query details of the certificate, this will give you a list of server-based ports that are open and have a certificate bound of them however, this report will be in an XML file.

The plan is also to scan a list of servers or the other example would be scan and network range, both will be covered in this particular guide.

The resulting scan will have one gigantic XML file that will have every server on the list/network range with an open port and we’re applicable, all the SSL certificate data inside that XML file.

NMAP Scan

The standard NMAP scan can be the command which is considered a quick scan.

nmap -T4 -F <servername>


That will return all the top ports on the servers up to TCP:1000 like this:


If you wish to scan all TCP ports then you can use this command:

nmap -p 1-65535 -T4 -A -v <servername>

If you wish to scan only 80 and 443 TCP ports then you can use this command:

nmap -p 80,443 -T4 -A -v <servername>

If you wish to scan all the TCP and UDP ports you can use this:

nmap -sS -sU -T4 -A -v <servername>

Finally if you want an intense scan you can use this, that will give you the full works for scans, it may take a moment of two as well:

nmap -T4 -A -v <servername>

NMAP Scan + ssl-cert

The next stage is to scan the server with the "ssl-cert" script added so you can return all the certificate information from the server with the open ports, in this example we will only scan port 443 (which is the common TCP port for HTTPS)

nmap -p 443 --script ssl-cert -v <servername>

This will return a the open ports as well as the certificate information along with it, which is the next phrase of this operation, as you can see below we now have the port TCP:443 and the certificate information as required:


NMAP Scan list of servers

We now need to scan a list of server names in a text file called server.txt and for this we can do this commands, this will scan all the ports on the servers in the servers.txt file and will report on all the ports that have certificates present into the XML file as below:

Note : The output file here is called open_ports_and_ssl.xml and the input file is server.txt

nmap -p- --script ssl-cert -oX open_ports_and_ssl.xml -iL servers.txt

Once you run this command, you will need to press space for an update, this will perform a connect scan then a NSE scan to get the scripted information into the XML file as below, the ETC is when the full scan will complete versus the current time which is very handy.


There are a couple of variations on this command as below:

Run with parallelism 

You should only really require this is packets are being dropped and this should only really be used for non-optimal network conditions, this example only scans TCP:80 and TCP:443:

nmap --min-parallelism 10 --max-parallelism 100 -p 443,80 --script ssl-cert -oX open_ports_and_ssl.xml -iL servers.txt

All host are online

This will skip the check to see if the host is actually online, this is only recommended if you are sure all hosts are online and available, this example only scans TCP:80 and TCP:443:

nmap -Pn -p 443,80 --script ssl-cert -oX open_ports_and_ssl.xml -iL servers.txt

NMAP Scan network range

If you wish to scan a network range then you can use this command, and on the end give the network CIDR, here we see the default home range on the end:

nmap -p- --script ssl-cert -oX open_ports_and_ssl.xml 192.168.1.0/24

XML Output File

Once the NMAP command nears completion like this, where I had 4 servers in my servers.txt the connect and NSE will have completed as below:

Stats: 0:21:39 elapsed; 0 hosts completed (4 up), 4 undergoing Script Scan
NSE Timing: About 99.64% done; ETC: 14:14 (0:00:04 remaining)

Then you will see a list of all the open ports and the certificate information in the XML as we witnessed earlier in the guide, so now you have a XML file that gives you all the information but its not formatted to a CSV file - that is where Python comes it people.

Python for parsing

Python is my choice here for parsing an XML file and I have a couple of goals here, I only want the columns listed like this:

IP,Hostname,Port,Service,Common Name (CN),SANs,Expiry Date

I also do not want to see all the ports that have no certificates attached to them, which would usually display "No Certificate" and I also do not want to see anything that has a certificate but has no common name (CN) as that would indicate it is not a certificate.

In order to run this python 🐍 you need to save this file with the extension “py” in this example, I called it SSL-normalize.py, the command below will run the scripts outlined in this post!

python3 ssl-normalize.py 

Now we have the requirements let get that done in Python, this is what I did to get these results:

import xml.etree.ElementTree as ET
import csv
from datetime import datetime

# Parse the nmap XML output
tree = ET.parse('open_ports_and_ssl.xml')
root = tree.getroot()

# Function to format date
def format_date(date_str):
    try:
        # Parse the date using the observed format
        date_obj = datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
        return date_obj.strftime('%d/%m/%Y')
    except ValueError:
        # If the date format is not as expected, return 'N/A'
        return 'N/A'

# Open a CSV file for writing (this will overwrite the file if it exists)
with open('output.csv', mode='w', newline='') as file:
    writer = csv.writer(file)
    writer.writerow(['IP', 'Hostname', 'Port', 'Service', 'Common Name (CN)', 'Expiry Date'])

    # Iterate through each host in the nmap XML
    for host in root.findall('host'):
        ip = host.find('address').get('addr')
        hostnames = host.find('hostnames')
        hostname = hostnames.find('hostname').get('name') if hostnames is not None else 'N/A'
        
        for port in host.find('ports').findall('port'):
            if port.find('state').get('state') == 'open':
                portid = port.get('portid')
                service_element = port.find('service')
                service = service_element.get('name', 'unknown') if service_element is not None else 'unknown'
                
                ssl_cert = port.find('.//script[@id="ssl-cert"]')
                if ssl_cert is not None:
                    # Initialize certificate details
                    cert_info = {
                        'CN': 'N/A',
                        'expiry': 'N/A'
                    }

                    # Extract CN from subject or DNS value
                    subject = ssl_cert.find('.//table[@key="subject"]/elem[@key="commonName"]')
                    if subject is not None and subject.text.startswith('CN='):
                        cert_info['CN'] = subject.text.split('CN=')[-1]
                    else:
                        dns_values = ssl_cert.findall('.//elem[@key="value"]')
                        for dns in dns_values:
                            if dns.text.startswith('DNS:'):
                                cert_info['CN'] = dns.text.split('DNS:')[-1]
                                break
                    
                    # Extract and format expiry date
                    notAfter = ssl_cert.find('.//elem[@key="notAfter"]')
                    if notAfter is not None:
                        notAfter_text = notAfter.text
                        cert_info['expiry'] = format_date(notAfter_text)
                        print(f"Expiry date raw text: {notAfter_text}, formatted: {cert_info['expiry']}")  

                    # Only write to CSV if a valid CN is found
                    if cert_info['CN'] != 'N/A':
                        writer.writerow([ip, hostname, portid, service, cert_info['CN'], cert_info['expiry']])
                else:
                    print(f"No SSL certificate found for IP {ip}, Port {portid}")

That then outputs exactly what I require and for many people that will do fine, but I need to get this to Windows SMB share for an import, so there is more to do from my point of view, first you will need to install the "smbprotocol" package to talk to these SMB shares, lets get that downloaded and install with this:

pip install smbprotocol

That should only take a couple of moments to install:


Then we can update the script to then output the CSV to to the root folder and save that file to a SMB share as well, which completes the mission, which can be done with this script, however before you can run the script you need to set some variables to avoid listing your password in the script:

export SMB_USERNAME=your_username
export SMB_PASSWORD=your_password

Then you can use this script without the password listed in the script, which would not be good security practice, then when the script completes it will clear those variables for you.

import os
import xml.etree.ElementTree as ET
import csv
from datetime import datetime
from smbprotocol.connection import Connection
from smbprotocol.session import Session
from smbprotocol.tree import TreeConnect
from smbprotocol.open import Open, CreateDisposition

# Function to format date
def format_date(date_str):
    try:
        # Parse the date using the observed format
        date_obj = datetime.strptime(date_str.strip(), '%Y-%m-%dT%H:%M:%S')
        return date_obj.strftime('%d/%m/%Y')
    except ValueError:
        # If the date format is not as expected, return 'N/A'
        return 'N/A'

# Parse the nmap XML output
tree = ET.parse('open_ports_and_ssl.xml')
root = tree.getroot()

# Define the output CSV file path
csv_file_path = 'output.csv'

# Open a CSV file for writing (this will overwrite the file if it exists)
with open(csv_file_path, mode='w', newline='') as file:
    writer = csv.writer(file)
    writer.writerow(['IP', 'Hostname', 'Port', 'Service', 'Common Name (CN)', 'SANs', 'Expiry Date'])

    # Iterate through each host in the nmap XML
    for host in root.findall('host'):
        ip = host.find('address').get('addr')
        hostnames = host.find('hostnames')
        hostname = hostnames.find('hostname').get('name') if hostnames is not None else 'N/A'
        
        for port in host.find('ports').findall('port'):
            if port.find('state').get('state') == 'open':
                portid = port.get('portid')
                service_element = port.find('service')
                service = service_element.get('name', 'unknown') if service_element is not None else 'unknown'
                
                ssl_cert = port.find('.//script[@id="ssl-cert"]')
                if ssl_cert is not None:
                    # Initialize certificate details
                    cert_info = {
                        'CN': 'N/A',
                        'expiry': 'N/A',
                        'SANs': 'N/A'
                    }

                    # Extract CN from output attribute
                    output = ssl_cert.get('output', '')
                    cn_start = output.find('Subject: commonName=')
                    if cn_start != -1:
                        cn_start += len('Subject: commonName=')
                        cn_end = output.find('\n', cn_start)
                        cert_info['CN'] = output[cn_start:cn_end].strip()

                    # Extract SANs from output attribute
                    san_start = output.find('Subject Alternative Name:')
                    if san_start != -1:
                        san_start += len('Subject Alternative Name:')
                        san_end = output.find('\n', san_start)
                        san_text = output[san_start:san_end].strip()
                        san_list = [san.split('DNS:')[-1].strip() for san in san_text.split(',') if 'DNS:' in san]
                        if san_list:
                            cert_info['SANs'] = ', '.join(san_list)

                    # Extract and format expiry date from output attribute
                    expiry_start = output.find('Not valid after:')
                    if expiry_start != -1:
                        expiry_start += len('Not valid after:')
                        expiry_end = output.find('\n', expiry_start)
                        notAfter_text = output[expiry_start:expiry_end].strip()
                        cert_info['expiry'] = format_date(notAfter_text)
                        print(f"IP: {ip}, Port: {portid}, Expiry date raw text: {notAfter_text}, formatted: {cert_info['expiry']}")  # Debugging line

                    # Only write to CSV if a valid CN is found
                    if cert_info['CN'] != 'N/A':
                        writer.writerow([ip, hostname, portid, service, cert_info['CN'], cert_info['SANs'], cert_info['expiry']])
                        print(f"Written to CSV: {ip}, {hostname}, {portid}, {service}, {cert_info['CN']}, {cert_info['SANs']}, {cert_info['expiry']}")  # Debugging line
                    else:
                        print(f"No valid CN found for IP {ip}, Port {portid}")  # Debugging line
                else:
                    print(f"No SSL certificate found for IP {ip}, Port {portid}")  # Debugging line

# Read SMB credentials from environment variables
smb_username = os.getenv('SMB_USERNAME')
smb_password = os.getenv('SMB_PASSWORD')
smb_domain = os.getenv('SMB_DOMAIN', '')
smb_server = 'drunkbear'
smb_share = 'smb'

if smb_username and smb_password:
    try:
        # Establish an SMB connection
        connection = Connection(uuid.uuid4(), smb_server)
        connection.connect()

        # Establish an SMB session
        session = Session(connection, smb_username, smb_password, smb_domain)
        session.connect()

        # Connect to the specified share
        tree = TreeConnect(session, f"\\\\{smb_server}\\{smb_share}")
        tree.connect()

        # Open the file on the SMB share
        smb_file_path = f"\\output.csv"
        smb_file = Open(tree, smb_file_path, access=0x2019F)
        smb_file.create(
            disposition=CreateDisposition.FILE_OVERWRITE_IF,
            options=0x200000)

        # Write the CSV file to the SMB share
        with open(csv_file_path, 'rb') as local_file:
            smb_file.write(local_file.read(), 0)
        smb_file.close()
        print(f"File uploaded successfully to {smb_server}\\{smb_share}{smb_file_path}")

    except Exception as e:
        print(f"Failed to upload file to SMB share: {e}")

    finally:
        # Clear SMB credentials from environment variables
        os.environ.pop('SMB_USERNAME', None)
        os.environ.pop('SMB_PASSWORD', None)
        os.environ.pop('SMB_DOMAIN', None)
else:
    print("SMB credentials are not set in the environment variables.")
Previous Post Next Post

Ω†Ω…ΩˆΨ°Ψ¬ Ψ§Ω„Ψ§ΨͺΨ΅Ψ§Ω„