RedRoom Recon Category : Hostprofile

What is the Hostprofile Tool?

The Hostprofile tool combines multiple methods to create a detailed network fingerprint of a target. This includes identifying the operating system, vendor, active ports, and MAC address. Such information is critical for both offensive and defensive cybersecurity operations, enabling a clear understanding of the target machine before taking further action.


How Can This Information Help?

Knowing a target’s operating system, vendor, and open ports is like having a blueprint of its defenses. It allows you to choose the right tools, select the most effective exploits, and plan the best approach whether you’re conducting a simulated attack as a red teamer or hardening systems as a defender.

For example:

  • An outdated OS could indicate known vulnerabilities.

  • A specific vendor might reveal default credentials or firmware quirks.

  • Open ports often point to running services that can be probed or exploited.

The more accurately you can profile a host, the more precise and effective your next steps become.


Method Flags in Hostprofile

The Hostprofile tool operates fully automatically the user does not need to select a specific method. The only required input is the IP range to be scanned.

Example Command (using 127.0.0.1):

python main.py hostprofile --range 127.0.0.1

Example Output :


Optional flags:

  • --timeout

  • --retries

  • --output

  • --format

Note: For detailed explanations of these common flags, see the earlier articles in this series.


Open the Hood : How Hostprofile Works

This is the handler function of hostprofile.py :

def run(args):
    if hasattr(args, "method") and args.method:
        print("[!] Invalid input: hostprofile does not require a method.")
        return

    if not validate_ip_range(args.range):
        return

    args.silent = True

    active_hosts = auto_hostdiscovery(
        args.range,
        args.timeout,
        args.retries,
        args.output,
        args.format,
        args.silent,
        extra_tcp_flags
    )

    if not active_hosts:
        print("[!] No active hosts found")
        return

    try:
        oui_map = load_oui("Essentials/oui.txt")
    except Exception as e:
        print(f"[!] Failed to load OUI database: {str(e)}")
        oui_map = {}

    for host in active_hosts:
        enhance_host_information(host, oui_map, args.timeout, args.retries, extra_tcp_flags)

    print_hostprofile_results(active_hosts)

Handler Function Overview


The handler function starts by confirming that no specific scanning method was provided by the user since Hostprofile is designed to run fully automatically. It then validates the given IP range to ensure it is formatted correctly.

Once the input is verified, the function proceeds to perform an automatic host discovery to identify active hosts within the specified range. If no active hosts are found, it prints a clear message indicating this outcome.

If active hosts are detected, the handler loads a local vendor database file (oui.txt) via the load_oui function. This database contains MAC address prefixes mapped to device vendors.


Finally, the handler calls the enhance_host_information function to enrich the collected data with vendor details, open ports, and other fingerprinting information.

def enhance_host_information(host, oui_map, timeout, retries, tcp_flags):
    mac = host.get("mac", "").strip().lower()
    if mac and mac != "unknown":
        host["vendor"] = lookup_vendor(mac, oui_map)

    results = PortScan.Scan_method_handler(host["ip"], tcp_flags, timeout, retries)
    for scan_type, scan_results in results.items():
        for result in scan_results:
            if result["ip"] == host["ip"]:
                host["ports"] = result["open_ports"]
                host["services"] = result["services"]

    try:
        os_detector = OSDetector()
        os_result = os_detector.run(host["ip"])

        host["os_data"] = {
            "primary_guess": os_result.get("primary_guess", "Unknown"),
            "confidence": os_result.get("confidence", "Unknown"),
            "alternatives": os_result.get("alternatives", []),
            "window_size": os_result.get("window_size"),
            "ttl": os_result.get("ttl")
        }

        primary = host["os_data"]["primary_guess"]
        alternatives = host["os_data"]["alternatives"]
        print(primary)
        print(alternatives)

        if primary.lower() == "unknown" and alternatives:
            host["os_data"]["primary_guess"] = alternatives.pop(0)
            host["os_data"]["alternatives"] = alternatives
        else:
            host["os_data"]["primary_guess"] = primary
            host["os_data"]["alternatives"] = alternatives

    except Exception as e:
        host["os_guess"] = "Detection failed"
        host["confidence"] = "low"
        host["os_data"] = {"error": str(e)}

Data Structure and Key Functions in Hostprofile


In the autohost_discovery process, I chose to use dictionaries to store host information. This approach keeps the data well-organized and easy to manage, with clear key names that describe each attribute.

Within the main enhancement function, three critical modules are called:

  • Lookup_vendor: Matches MAC addresses against the local vendor database to identify device manufacturers.

  • Portscan: Performs a thorough scan to discover which ports and services are active on the target.

  • OSdetector: Estimates the operating system based on network packet characteristics such as response times, TTL values, and TCP window sizes.

Each of these modules serves a distinct role, combining their results to build a detailed profile of the target host.


Recap of Core Modules

To summarize, the key modules developed and used in Hostprofile are:

  • autohost_discovery — for discovering active hosts within an IP range and gathering basic information.

  • Lookup_vendor — for identifying the vendor from MAC addresses.

  • Portscan — for detecting open ports and active services.

  • OSdetector — for estimating the target's operating system based on network behavior.


Auto Host Discovery

Complete code of the auto_host.py file:

import ipaddress
from ..protocol_scans.arp_scan import ARPScan
from ..protocol_scans.icmp_scan import ICMPScan
from ..protocol_scans.tcp_scan import Handler
from ..protocol_scans.udp_scan import UDPScan

def is_local_network(target_range):
    try:
        network = ipaddress.ip_network(target_range, strict=False)
    except ValueError:
        print(f"[!] Invalid IP or network: {target_range}")
        return False
    return network.network_address.is_private

def auto_hostdiscovery(target_ip, timeout, retries, filename, ftype, silent, extra_tcp_flags):
    results = []
    if is_local_network(target_ip):
        print("[*] Local network detected, using ARP scan...")
        try:
            results = ARPScan.arp_scan(target_ip, timeout, retries, filename, ftype, silent, max_workers=50)

            if not any(r['status'] == 'ACTIVE' for r in results):
                print("[!] No active hosts found with ARP, falling back to ICMP scan...")
                results = ICMPScan.icmp_scan(target_ip, timeout, retries, filename, ftype, silent)

                if not any(r['status'] == 'ACTIVE' for r in results):
                    print("[!] No active hosts found with ICMP, trying UDP scan...")
                    results = UDPScan.udp_scan(target_ip, timeout, retries, filename, ftype, silent)

        except Exception as e:
            print(f"[!] ARP scan error: {e}, falling back to ICMP scan...")
            results = ICMPScan.icmp_scan(target_ip, timeout, retries, filename, ftype, silent)
            if not any(r['status'] == 'ACTIVE' for r in results):
                print("[!] No active hosts found with ICMP, trying UDP scan...")
                results = UDPScan.udp_scan(target_ip, timeout, retries, filename, ftype, silent)

    else:
        print("[*] Remote network detected, using ICMP scan...")
        try:
            results = ICMPScan.icmp_scan(target_ip, timeout, retries, filename, ftype, silent)
            if not any(r['status'] == 'ACTIVE' for r in results):
                print("[!] No active hosts found with ICMP, trying TCP scan...")
                results = Handler.tcp_scan(target_ip, extra_tcp_flags, timeout, retries, filename, ftype)
                if not any(r['status'] == 'ACTIVE' for r in results):
                    print("[!] No active hosts found with TCP, trying UDP scan...")
                    results = UDPScan.udp_scan(target_ip, timeout, retries, filename, ftype, silent)
        except Exception as e:
            print(f"[!] ICMP scan error: {e}, trying TCP scan...")
            results = Handler.tcp_scan(target_ip, extra_tcp_flags, timeout, retries, filename, ftype)
            if not any(r['status'] == 'ACTIVE' for r in results):
                print("[!] No active hosts found with TCP, trying UDP scan...")
                results = UDPScan.udp_scan(target_ip, timeout, retries, filename, ftype, silent)

    active_hosts = [host for host in results if host["status"] == "ACTIVE"]
    return active_hosts

As demonstrated in the auto_host module, this process automates host discovery compared to the more manual hostscan approach. While it provides less direct control to the user, it simplifies and speeds up the overall workflow. The core logic begins with an ARP scan on the local network (LAN) to identify active hosts. If the ARP scan fails, the tool automatically falls back to alternative discovery methods sometimes multiple times until at least one host is found. Importantly, the function returns a well-structured dictionary containing host information, which is then passed to enhance_host_information for further processing. This clear, consistent data format helps keep the entire process organized and easy to manage.


Lookup Vendor

Complete code of the vendor_lookup.py file:

from scapy.all import ARP, Ether, srp

def load_oui(filename):
    oui_map = {}
    with open(filename, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            if "(hex)" in line:
                parts = line.strip().split()
                if len(parts) >= 3:
                    key = parts[0].replace("-", ":").lower()
                    vendor = " ".join(parts[2:])
                    oui_map[key] = vendor
    return oui_map      

def lookup_vendor(mac, oui_map):
    if not mac:
        return "Unknown"

    mac_prefix = mac.lower()[0:8]
    return oui_map.get(mac_prefix, "Unknown")

This code uses Scapy to help identify device vendors on a network based on their MAC addresses. The load_oui function reads an IEEE OUI (Organizationally Unique Identifier) file and builds a dictionary mapping MAC address prefixes to vendor names. The lookup_vendor function then takes a MAC address, extracts its first 8 characters (the OUI), and checks this dictionary to find the associated manufacturer. If no match is found, it returns "Unknown". This allows network scans to not only list devices but also show who made them.


OS Detector

Snippet 1 – Class Setup and Mappings

class OSDetector:
    def __init__(self):
        logging.basicConfig(level=logging.INFO)
        self.logger = logging.getLogger("OSDetector")

        self.common_ports = [
            21, 22, 23, 25, 53, 80, 110, 143, 443, 445, 
            3389, 8080, 8443
        ]

        self.candidates_window = []
        self.candidates_ttl = []
        self.os_mapping = {
            32: ["Windows 95/98/ME"],
            64: ["Linux", "macOS", "FreeBSD", "Unix variants"],
            128: ["Windows NT/2000/XP/7/8/10/11"],
            255: ["Cisco devices", "Network equipment"],
            0: ["Unknown"]
        }

        self.os_signatures = {
            65535: ["Windows 10/11", "Windows Server 2016+", "macOS (recent)"],
            8192: ["Windows 7/8", "Windows Server 2008/2012"],
            16384: ["Windows Vista"],
            64240: ["Windows (some configurations)"],
            5840: ["Linux (Ubuntu/Debian)"],
            5792: ["Linux (CentOS/RHEL)"],
            29200: ["Linux (recent kernels)"],
            32768: ["macOS (older)", "iOS"],
            4128: ["Cisco IOS"],
            5720: ["JunOS"],
            0: ["Unknown"]
        }

Here I initialize the OSDetector class, set up logging, and define two important data sources:

  • common_ports: The most frequently used service ports we’ll probe for OS hints.

  • os_mapping and os_signatures: Dictionaries that map TTL values and TCP window sizes to possible operating systems.

These mappings are the “fingerprints” we’ll match against during detection.


Snippet 2 – TCP SYN Scans for Window Size

def send_syn(self, target_ip, target_port):
    try:
        syn_packet = IP(dst=target_ip)/TCP(
            dport=target_port,
            flags="S",
            options=[('MSS', 1460)]
        )

        response = sr1(syn_packet, timeout=2, verbose=0)

        if response and response.haslayer(TCP):
            if response[TCP].flags == 0x12:
                window_size = response[TCP].window

                rst_packet = IP(dst=target_ip)/TCP(
                    dport=target_port,
                    sport=response[TCP].dport,
                    flags="R",
                    seq=response[TCP].ack
                )
                send(rst_packet, verbose=0)
                return target_port, window_size

        return target_port, None

    except Exception as e:
        self.logger.debug(f"Port {target_port} scan failed: {str(e)}")
        return target_port, None

This method sends a SYN packet to a target port and checks the response.

  • If we get a SYN-ACK (flags 0x12), the connection attempt succeeded and we can record the TCP window size.

  • We then send an RST packet to politely close the connection.

  • That window size is a key clue for identifying the OS.


Snippet 3 – TTL Probing

def send_echo_pings(self, target_ip):
    responses = []
    try:
        for pkt in [
            IP(dst=target_ip)/ICMP(),
            IP(dst=target_ip)/TCP(dport=80, flags="S")
        ]:
            responses.append(sr1(pkt, timeout=2, verbose=0))
            time.sleep(1)

        ttls = [
            r[IP].ttl for r in responses 
            if r and IP in r and r[IP].ttl is not None
        ]

        return ttls if ttls else None

    except Exception as e:
        self.logger.debug(f"Ping failed: {str(e)}")
        return None

This function sends two lightweight probes:

  1. An ICMP echo request (classic ping).

  2. A TCP SYN to port 80.

It records the Time-To-Live (TTL) values from responses.
Different OSes have different default starting TTL values (32, 64, 128, 255), which become another fingerprint clue.


Snippet 4 – Analyzing and Matching Fingerprints

def calculate_original_ttl(self, received_ttl):
    if received_ttl is None:
        self.candidates_ttl.append(0)
        return

    initial_ttls = [32, 64, 128, 255]
    closest = min(initial_ttls, key=lambda x: abs(x - received_ttl))
    self.candidates_ttl.append(closest)

def analyse_window_size(self, window_size):
    if window_size is None: 
        self.candidates_window.append(0)
        return

    if window_size in self.os_signatures:
        self.candidates_window.append(window_size)
        return

    for scale in [1, 2, 4, 8]:
        scaled = window_size * scale
        for known_size in self.os_signatures:
            if abs(scaled - known_size) <= 10:
                self.candidates_window.append(known_size)
                return

    self.candidates_window.append(0)

Explanation:

  • calculate_original_ttl() adjusts measured TTL to the nearest known starting value.

  • analyse_window_size() compares observed TCP window sizes against known OS signatures, allowing for scaling differences.

This is where raw probe data turns into OS candidate matches.


Snippet 5 – Parallel Scanning

def scan_multiple_ports(self, target_ip, max_threads=50):
    def task(port):
        _, window_size = self.send_syn(target_ip, port)
        self.analyse_window_size(window_size)
        return window_size

    with concurrent.futures.ThreadPoolExecutor(
        max_workers=min(max_threads, len(self.common_ports))
    ) as executor:
        futures = [executor.submit(task, port) for port in self.common_ports]
        concurrent.futures.wait(futures)

I use threading to scan multiple ports in parallel, speeding up data collection for OS detection.


Snippet 6 – Combining Results and Reporting

def find_best_guess(self):
    window_counter = Counter(self.candidates_window)
    ttl_counter = Counter(self.candidates_ttl)

    most_common_window = window_counter.most_common(1)[0][0] if window_counter else 0
    most_common_ttl = ttl_counter.most_common(1)[0][0] if ttl_counter else 0

    window_os = self.os_signatures.get(most_common_window, ["Unknown"])
    ttl_os = self.os_mapping.get(most_common_ttl, ["Unknown"])

    window_confidence = window_counter[most_common_window] / sum(window_counter.values()) if window_counter else 0
    ttl_confidence = ttl_counter[most_common_ttl] / sum(ttl_counter.values()) if ttl_counter else 0

    combined_confidence = (window_confidence * 0.7) + (ttl_confidence * 0.3)

    common_guesses = list(set(window_os) & set(ttl_os))
    if not common_guesses:
        common_guesses = window_os + ttl_os

    return {
        "window_data": {
            "value": most_common_window,
            "count": window_counter.get(most_common_window, 0),
            "possible_os": window_os,
            "confidence": window_confidence
        },
        "ttl_data": {
            "value": most_common_ttl,
            "count": ttl_counter.get(most_common_ttl, 0),
            "possible_os": ttl_os,
            "confidence": ttl_confidence
        },
        "combined_confidence": combined_confidence,
        "os_guesses": common_guesses
    }

This method merges TTL-based guesses and window-size-based guesses into a final OS prediction with a calculated confidence level.
The output is structured, making it easy for other tools (or your UI) to display results.


Snippet 7 – Main Run Method

def run(self, target_ip):
    self.logger.info(f"Starting OS detection for {target_ip}")

    self.candidates_window = []
    self.candidates_ttl = []

    try:
        start_time = time.time()

        self.logger.info("Starting port scan phase...")
        self.scan_multiple_ports(target_ip)

        self.logger.info("Starting TTL probe phase...")
        self.scan_ttl_probes(target_ip)

        results = self.find_best_guess()
        duration = time.time() - start_time

        combined_confidence = results.get("combined_confidence", 0)
        if combined_confidence > 0.7:
            confidence_str = "high"
        elif combined_confidence > 0.4:
            confidence_str = "medium"
        else:
            confidence_str = "low"

        os_guesses = results.get("os_guesses", [])
        primary_guess = os_guesses[0] if os_guesses else "Unknown"
        alternatives = os_guesses[1:] if len(os_guesses) > 1 else []

        report = {
            "primary_guess": primary_guess,
            "confidence": confidence_str,
            "alternatives": alternatives,
            "window_size": results["window_data"]["value"],
            "ttl": results["ttl_data"]["value"],
            "duration_seconds": round(duration, 2),
            "scanned_ports": len(self.common_ports)
        }

        self.logger.info(f"Scan completed in {duration:.2f} seconds")
        return report

    except Exception as e:
        self.logger.error(f"Scan failed: {str(e)}")
        return {
            "error": str(e),
            "target": target_ip,
            "status": "failed"
        }

This is the entry point for the OS detection process. It:

  • Runs the port scanning phase.

  • Runs the TTL probe phase.

  • Combines the results into a clear, easy-to-understand report that includes confidence levels and timing information.


Conclusion

Hostprofile is the second tool in the RedRoom suite, and as you can see, there’s a lot to uncover about how it works. What matters most is that it provides clear, actionable information about the target machine. Each module plays its part, leveraging Scapy, the ipaddress library, and protocol-specific scans. I’m keeping the Portscan module under wraps for now, as we’ll dive into it in detail in the upcoming Portscan article.


Next up: RedRoom’s Recon Category: Portscan

0
Subscribe to my newsletter

Read articles from Ektoras Kalantzis directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ektoras Kalantzis
Ektoras Kalantzis