Operating System Enumeration

There are little clues that can be gathered when first approaching a target as to the operating system and version. This cheat sheet is meant to showcase three methods for pulling information from initial scans. First I’ll look at SSH and webserver application versions and use them to map to OS versions. Then I’ll look at ports that are commonly present on Windows DCs and clients. Finally, I’ll look at IP packet TTL values, and how they can identify an OS, as well as virtualized systems.
Software Versions
Overview
When running an nmap
scan with -sV
for version scan will give information about the software versions running on the host. For systems with package managers, it’s very common for different versions of common software to be tied to different versions of the operating system. That isn’t to say they can’t run on a different system, just that they are most likely to show up on a specific version.
Ubuntu
Ubuntu releases two versions a year, with even-yeared .04 versions being the long term support (LTS) versions. Each release gets a two word name with an adjective and animal starting with the same letter, though it is common to refer to each version by just it’s adjective (“focal” rather than “focal fossa”). For the most part the letter increments with each release
The main software I’ve used for this kind of fingerprinting is OpenSSH and Apache. nginx is also worth considering, the the same versions show up on many different versions.
The packages for the supported distributions can be found at https://packages.ubuntu.com/search?keywords=<software>
. Unsupported OS versions have been removed, though I’ve kept a list.
Ubuntu Version | OpenSSH | Apache | nginx |
---|---|---|---|
14.04 - trusty [LTS] | 6.6p1 | 2.4.7 | 1.4.6 |
14.10 - utopic | 6.6p1 | 2.4.10 | 1.6.2 |
15.04 - vivid | 6.7p1 | 2.4.12 | 1.6.2 |
15.10 - wily | 6.9p1 | 2.4.12 | 1.6.2 |
16.04 - xenial [LTS] | 7.2p1 | 2.4.18 | 1.10.0 |
16.10 - yakketty | 7.2p1 | 2.4.18 | 1.10.0 |
17.04 - zesty | 7.4p1 | 2.4.25 | 1.12.0 |
17.10 - artful | 7.6p1 | 2.4.27 | 1.13.3 |
18.04 - bionic [LTS] | 7.6p1 | 2.4.29 | 1.14.0 |
18.10 - cosmic | 7.7p1 | 2.4.34 | 1.16.0 |
19.04 - disco | 7.9p1 | 2.4.35 | 1.16.0 |
19.10 - eoan | 7.9p1 | 2.4.41 | 1.17.3 |
20.04 - focal [LTS] | 8.2p1 | 2.4.41 | 1.18.0 |
20.10 - groovy | 8.2p1 | 2.4.46 | 1.18.0 |
21.04 - hirsute | 8.4p1 | 2.4.48 | 1.20.1 |
21.10 - impish | 8.4p1 | 2.4.51 | 1.20.1 |
22.04 - jammy [LTS] | 8.9p1 | 2.4.52 | 1.18.0 |
22.10 - kinetic | 8.9p1 | 2.4.52 | 1.22.0 |
23.04 - junar | 9.0p1 | 2.4.54 | 1.24.0 |
23.10 - mantic | 9.3p1 | 2.4.57 | 1.24.0 |
24.04 - noble [LTS] | 9.6p1 | 2.4.58 | 1.24.0 |
24.10 - oracular | 9.7p1 | 2.4.62 | 1.26.0 |
25.04 - plucky | 9.9p1 | 2.4.63 | 1.26.3 |
When there are security updates, sometimes these will increment in the final digit.
Debian
Debian has many fewer versions when compared to Ubuntu. They release every two years in odd years and each have a codeword as a character from the Toy Story movies.
OpenSSH and NGINX all are good indicators here, while Apache seems to update as long as the OS is supported, which is typically around 5 years.
Debian Version| [Release Year] |
OpenSSH | nginx |
---|---|---|
8 - Jessie [2015] | 6.7p1 | 1.6.2 |
9 - Stretch [2017] | 7.4p1 | 1.10.3 |
10 - Buster [2019] | 7.9p1 | 1.42.2 |
11 - Bullseye [2021] | 8.4p1 | 1.8.0 |
12 - Bookworm [2023] | 9.2p1 | 1.22.1 |
These can change slightly as well.
Red Hat / Centos
The packages associated with different versions of Red Hat / CentOS are less documented. Based on very limited experience, I’ve see the following Apache and OpenSSH versions:
Red Hat / CentOS Version |
OpenSSH | Apache |
---|---|---|
5 | 5.3p1 | 2.2.3 |
6 | 6.6p1 | 2.2.15 |
7 | 7.4p1 | 2.4.6 |
8 | 8.0p1 | 2.4.37 |
9 | 9.1p1 | 2.4.53 |
Windows
Windows IIS versions used to track the OS version, but since Windows 10 / Server 2016 and after, have continued to use the same IIS version, 10.0, making this less and less useful over time:
Windows Version | IIS |
---|---|
Windows 10 / Server 2016 and later | Microsoft IIS httpd 10.0 |
Windows 8.1 / Server 2012 R2 | Microsoft IIS httpd 8.5 |
Windows 7 / Server 2008 R2 | Microsoft IIS httpd 7.5 |
Windows XP (x64) / Server 2003 | Microsoft IIS httpd 6.0 |
Common Ports
Windows Domain Controller
A windows domain controller has many ports that are required to be open for it to function as a DC:
- UDP 123 - NTP (W32Time)
- TCP/UDP 53 - DNS
- TCP/UDP 88 - Kerberos
- TCP 135 - RPC
- TCP/UDP 389 - LDAP
- TCP 445 - SMB
- TCP/UDP 464 - Kerberos password change
- TCP 636 - LDAP SSL
- TCP 3268 - LDAP GC
- TCP 3269 - LDAP GC SSL
Seeing this combination (or even a subset if the firewall may be blocking some) is a good indication of a Windows Domain controller.
Windows Client
There are other ports that are commonly seen on Windows hosts:
- TCP 135 - RPC
- TCP 139 - NetBios
- TCP 445 - SMB
Linux has an implementation of SMB (Samba), but the version string should make it very obvious if this is the case. If SMB is open, a tool like NetExec can typically get the full OS information (and host and domain name), as shown on my SMB Enumeration Cheatsheet:
oxdf@hacky$ netexec smb 10.10.11.236
SMB 10.10.11.236 445 DC01 Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:manager.htb) (signing:True) (SMBv1:False)
There are more ports that are less common, but typically are solid indications of a Windows host:
- TCP 1433 - MSSQL
- TCP 3389 - Remote Desktop (RDP)
- TCP 5895 - WinRM
Linux
Linux clients / servers don’t have as much the standard set of ports that Windows does. Things like SSH (TCP 22) or MySQL (TCP 3306) are much more common on Linux, but can be found on Windows.
TTL
Background
When an IP packet is sent, it has a time-to-live (TTL) field. This field is an 8-bit value ranging from 225 to 0. Each hop along the path from the source to the destination is supposed to decrement the TTL field by one.
OS Identification
Different OSes initialize the TTL field to different values (for different protocols). There are some nice blog posts out there (like this from Subin’s Blog and this from OStechNix) that have reference tables for lots of different OSes. The high level rule of thumb I’ve always used is:
- Linux / MAC - 64
- Windows - 128
- Routers / Network Devices - 255
Container / VM Identification
Strategy
Looking at the TTLs for different ports on the same target can show if there’s some kind of routing going on behind it (in some cases). For a real-world target, this could be routing to other hosts. In something like HackTheBox, it typically suggests virtualization like a VM or container.
Linux
On Linux, when a packet is passed from the host to a VM or on container, it typically does decrement the TTL. That means that I can track what ports are part of the host and which are in the container (sometimes).
For example, the Resource box from HTB has an nmap
that looks like:
oxdf@hacky$ nmap -p 22,80,2222 -sCV 10.10.11.27
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-04-10 19:21 UTC
Nmap scan report for 10.10.11.27
Host is up (0.092s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
| ssh-hostkey:
| 256 78:1e:3b:85:12:64:a1:f6:df:52:41:ad:8f:52:97:c0 (ECDSA)
|_ 256 e1:1a:b5:0e:87:a4:a1:81:69:94:9d:d4:d4:a3:8a:f9 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://itrc.ssg.htb/
2222/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 f2:a6:83:b9:90:6b:6c:54:32:22:ec:af:17:04:bd:16 (ECDSA)
|_ 256 0c:c3:9c:10:f5:7f:d3:e4:a8:28:6a:51:ad:1a:e1:bf (ED25519)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 10.19 seconds
Right away I suspect a VM or container, as the SSH on 22 shows Debian while the SSH on 2222 shows Ubuntu.
lft
will show a TCP traceroute to a given port:
oxdf@hacky$ sudo lft 10.10.11.27:2222
Tracing ......T
TTL LFT trace to 10.10.11.27:2222/tcp
1 10.10.14.1 92.1ms
2 [target open] 10.10.11.27:2222 92.1ms
oxdf@hacky$ sudo lft 10.10.11.27:22
Tracing ......T
TTL LFT trace to 10.10.11.27:22/tcp
1 10.10.14.1 92.5ms
2 10.10.11.27 97.4ms
3 [target open] 10.10.11.27:22 93.0ms
This shows that 2222 is on the host where as 22 is another hop in.
I can try 80 as well:
oxdf@hacky$ sudo lft 10.10.11.27:80
Tracing ......T
TTL LFT trace to 10.10.11.27:80/tcp
1 10.10.14.1 91.9ms
2 [target open] 10.10.11.27:80 93.2ms
It shows that the site is hosted on the host. This result is a bit deceptive, and it’s important to understand what’s happening. The host is running nginx, which is proxying to the container where Apache is serving the site. So my TCP connection is with nginx, and it creates another TCP connection to the container. So the site really is another hop in, but that hop isn’t taking place on my TCP connection to see it.
Windows
Windows doesn’t typically decrement the TTL when routing to virtual machines or containers. For example, the Ghost machine from HTB has multiple Hyper-V VMs running on it, including an Ubuntu one. Yet they all show up as the same number of hops away with lft
. For example, 80 is on the Windows host but 8008 in a Docker container in an Ubuntu VM:
oxdf@hacky$ sudo lft ghost.htb:80
Tracing ......T
TTL LFT trace to DC01 (10.10.11.24):80/tcp
1 10.10.14.1 91.8ms
2 [target open] DC01 (10.10.11.24):80 92.4ms
oxdf@hacky$ sudo lft ghost.htb:8008
Tracing ......T
TTL LFT trace to DC01 (10.10.11.24):8008/tcp
1 10.10.14.1 92.3ms
2 [target open] DC01 (10.10.11.24):8008 94.3ms
They both show just two hops because of how Windows is handling the packets.