Most network services are polite. The moment you connect, before you ask for anything, they introduce themselves: their software name, often their version. That introduction is called a banner, and reading one takes about ten lines of Python. It is also, quietly, the idea behind one of the most famous tools in security.
Analogy: The shop signboard
Walk down a market street and you can tell what each shop sells without entering, because every shop hangs a signboard. Many network services do the same: the moment you connect, before you ask anything, they announce their software name and version in the first few bytes they send.
Reading that announcement is called banner grabbing, and it is often enough to tell an analyst whether a host is running old, unpatched software.
Step 1: turn a name into an address
A hostname must be turned into an IP address first, the way a name in a phone book is turned into a number:
resolve.py
import socket
ip = socket.gethostbyname("example.com")
print(f"example.com resolves to {ip}") # a dotted IPv4 address
try:
hostname, _, _ = socket.gethostbyaddr(ip)
print(f"Reverse lookup: {hostname}")
except socket.herror:
print("No reverse DNS record for this address")
Sample output (the address will differ; it can change over time)
example.com resolves to 104.20.23.154
No reverse DNS record for this address
gethostbyname asks DNS "what is the number for this name?". gethostbyaddr asks the reverse: "what is the name for this number?". Many addresses have no reverse record, which is why that call is wrapped in a try and the socket.herror is caught. A name that does not exist at all raises a different error, socket.gaierror, which you will meet in the exception-handling article.
Step 2: read the banner
Then the banner grab itself: connect to an open port and read whatever greeting arrives before any request is made.
banner_grab.py
# banner_grab.py
import socket
def grab_banner(host, port, timeout=2.0):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect((host, port))
banner = sock.recv(1024)
return banner.decode(errors="replace").strip()
except (socket.timeout, ConnectionRefusedError):
return None
finally:
sock.close()
banner = grab_banner("10.0.0.5", 22)
if banner:
print(f"Banner: {banner}") # e.g. SSH-2.0-OpenSSH_7.4
else:
print("No banner received, or the port is closed")
Sample output
Banner: SSH-2.0-OpenSSH_7.4
| Part of the function | Why it is there |
|---|---|
sock.settimeout(timeout) | A silent host must not hang your script forever; give up after two seconds |
sock.connect((host, port)) | The three-way handshake. If the door is closed, this raises ConnectionRefusedError |
sock.recv(1024) | Listen for the greeting. A polite service has already sent it |
.decode(errors="replace").strip() | Bytes become readable text; stray bytes become a placeholder; the trailing newline goes |
except (socket.timeout, ConnectionRefusedError) | Silent host or closed door: return None instead of crashing |
finally: sock.close() | Whatever happened, hang up. Cleanup always lives in finally |
That one line, SSH-2.0-OpenSSH_7.4, carries two facts an analyst acts on at once: the service is OpenSSH, and its version is 7.4. A quick search of a vulnerability database against "OpenSSH 7.4" returns every weakness fixed in later versions, which is the next move in any assessment.
Practise safely: a lab with three pretend services
You should not point a banner grabber at a machine you do not own, so build a safe target. This script starts three pretend services on your own computer. Each one speaks first, like a real SSH, FTP or mail server, and then hangs up. (It uses threads so the three can run together. You will meet threads properly two articles from now.)
lab_banners.py
# lab_banners.py: three pretend services for banner-grabbing practice.
# Run it on your own machine only. It speaks first, like a real SSH, FTP or mail server.
import socket
import threading
SERVICES = {
9991: b"SSH-2.0-OpenSSH_7.4\r\n",
9992: b"220 ProFTPD 1.3.5 Server ready.\r\n",
9993: b"220 mail.lab.local ESMTP Postfix\r\n",
}
def serve(port, banner):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", port))
server.listen(5)
while True:
client, _ = server.accept()
client.sendall(banner) # announce ourselves before being asked
client.close()
for port, banner in SERVICES.items():
threading.Thread(target=serve, args=(port, banner), daemon=True).start()
print("Lab services up on 127.0.0.1:", ", ".join(str(p) for p in SERVICES), flush=True)
threading.Event().wait() # keep the program alive
With that running, save grab_banner() in banner_grab.py (with the if __name__ == "__main__": guard from the modules article around the demo lines) and sweep five ports. Start a throwaway web server on 9994 too (python3 -m http.server 9994 --bind 127.0.0.1) so you can see what a service that does not speak first looks like:
sweep_banners.py
from banner_grab import grab_banner
for port in (9991, 9992, 9993, 9994, 9995):
banner = grab_banner("127.0.0.1", port, timeout=1.0)
print(f"{port}: {banner if banner else '(nothing)'}")
Sample output
9991: SSH-2.0-OpenSSH_7.4
9992: 220 ProFTPD 1.3.5 Server ready.
9993: 220 mail.lab.local ESMTP Postfix
9994: (nothing)
9995: (nothing)
Three ports greeted you with a name and a version. Port 9994 is open but silent, and port 9995 is simply closed. Both give (nothing), but for different reasons, and that difference is the heart of the lab exercise at the end of this unit.
Python concept: Some services wait for you to speak first
A mail, FTP or SSH server announces itself. A web server does not: it waits for a request. grab_banner() listens without sending anything, so against a web server it hears silence until the timeout. To read a web server's banner you must send a request first:
import socket
s = socket.socket(); s.settimeout(2)
s.connect(("127.0.0.1", 9994))
s.sendall(b"HEAD / HTTP/1.0\r\n\r\n") # ask politely
reply = s.recv(1024).decode(errors="replace")
print([line for line in reply.splitlines() if line.lower().startswith("server")])
['Server: SimpleHTTP/0.6 Python/3.14.8']
Different protocols, different manners. A good banner grabber knows which services speak first.
From a banner to a finding
A banner is raw text. To act on it, a script must pull out the software name and the version. That is a string-handling job, and you already know the tools:
parse_banner.py
def parse_ssh_banner(banner):
# SSH-2.0-OpenSSH_7.4 -> protocol 2.0, software OpenSSH, version 7.4
parts = banner.split("-", 2)
protocol = parts[1]
software, _, version = parts[2].partition("_")
return protocol, software, version
for b in ["SSH-2.0-OpenSSH_7.4", "SSH-2.0-dropbear_2020.81", "SSH-2.0-OpenSSH_9.6"]:
protocol, software, version = parse_ssh_banner(b)
print(f"{b:<26} protocol {protocol} software {software:<9} version {version}")
Sample output
SSH-2.0-OpenSSH_7.4 protocol 2.0 software OpenSSH version 7.4
SSH-2.0-dropbear_2020.81 protocol 2.0 software dropbear version 2020.81
SSH-2.0-OpenSSH_9.6 protocol 2.0 software OpenSSH version 9.6
Now a script can sweep a hundred hosts, parse every banner, and list which ones announce an old version. That list is the first page of a vulnerability report. Equifax shows why it must not stay a list.
Real incident: Equifax, 2017: the scan found it, nobody acted
In 2017, the credit-reporting agency Equifax suffered a breach that exposed the personal data of roughly 147 million people. The way in was a known vulnerability in the Apache Struts web framework, catalogued as CVE-2017-5638, for which a patch had been available since March 2017, months before the breach. Investigations afterward found that Equifax's own scanning had not reliably flagged the vulnerable system, and the patch was never applied to it.
What this teaches: knowing what software and version a system runs is only half the job. The other half is closing the loop between the finding and the fix. A banner sweep is the cheap way to learn the first half. A person or a process must do the second.
A banner is a claim, not proof
A returned banner such as SSH-2.0-OpenSSH_7.4 names the exact software and version, which an analyst can look up against a vulnerability database in seconds. But a banner is a claim a service makes about itself, not proof, and a well-hardened host may suppress or fake it on purpose. Here is a service that lies. It is a few lines of Python, yet it announces itself as a recent OpenSSH:
spoof.py
# a lab service that LIES: it is a few lines of Python, but it claims to be a newer OpenSSH
import socket
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 9996)); s.listen(5)
while True:
c, _ = s.accept(); c.sendall(b"SSH-2.0-OpenSSH_9.6\r\n"); c.close()
grab_banner("127.0.0.1", 9996)
Banner: SSH-2.0-OpenSSH_9.6
Your banner grabber reports a modern OpenSSH, and there is no OpenSSH there at all. Treat a banner as the first clue in an investigation, never the last word. Confirm with a second source before you write a finding.
Case study: Shodan: a search engine built from banners
Shodan, launched in 2009 by John Matherly, is often described as "the search engine for internet-connected devices". It works by doing, continuously and at the scale of the entire public internet, exactly what the grab_banner() function above does to one host: it connects to common ports across billions of addresses and records the banner each service returns. The result is a searchable index of what software every reachable device on the internet is running, from web servers and databases to industrial control systems, traffic cameras and building management panels.
Shodan is used every day by defenders to find their own organisation's exposed systems before an attacker does, and by researchers to measure how long a newly disclosed vulnerability takes to be patched across the internet. The same index, of course, is equally visible to attackers looking for unpatched versions.
What this teaches: banner grabbing is not a toy exercise. An entire industry of attack-surface management is built on the handful of lines you just read, and the lesson for a defender is blunt: if a service announces an old, vulnerable version to anyone who connects, assume an automated index has already recorded it.
Caution: Authorised systems only
Run grab_banner() only against hosts inside your own lab network. Pointing a banner grabber at a system you do not own is unauthorised access under Sections 43 and 66 of the Information Technology Act, 2000. The practice services above exist so that you never need to.
Check your understanding
Question 1: A banner grab against a hardened host returns nothing. Give two different reasons this might happen that do not involve the port being closed.
Any two of these: the service is one that waits for the client to speak first (like a web server), so there is nothing to read until you send a request; the service has been configured to suppress its banner; a firewall is silently dropping your connection so the read times out; the host replies slowly and your timeout was too short.
Question 2: Why is a banner "a claim, not proof"?
The banner is simply text the service chooses to send. A hardened host can hide it or change it, and a fake service can announce any name and version it likes, as spoof.py does. It is a first clue to be confirmed, not a finding on its own.
Question 3: What does Shodan have to do with the grab_banner() function?
Shodan does at internet scale what grab_banner() does to one host. It connects to common ports across billions of addresses, records each banner, and makes the result searchable.
Key takeaway: Key takeaway
Resolve the name, connect, read the greeting, and treat what you read as a clue. A banner names the software and version at a glance, which is why defenders and attackers both collect them, and why an old version announced to the world should be assumed to have been recorded already.

