Telnet — Why You'll (Almost) Never Use It
15 min In ProgressLearning Objectives
- Explain why Telnet credentials are visible to anyone on the wire
- Watch plaintext credentials cross a real (local) socket
- Know when Telnet is still defensible to use
Why This Tool Still Exists
Telnet does the same job as SSH — open a remote CLI session — but it predates encryption being standard. Every password and command travels as plain, readable bytes. You'll only reach for it against genuinely ancient lab gear with no SSH option, or inside a controlled lab/CTF environment where seeing the danger first-hand is the point of the exercise.
The Real Driver (Lab / Real-Device Code)
The shape is the same connect / send_command / close pattern as every other driver in this chapter.
import telnetlib
import time
class telnet_driver:
def __init__(self, hostname, username, password, timeout=5):
self.hostname = hostname
self.username = username
self.password = password
self.timeout = timeout
self.tn = None
def connect(self):
self.tn = telnetlib.Telnet(self.hostname, timeout=self.timeout)
self.tn.read_until(b'Username: ', self.timeout)
self.tn.write(self.username.encode() + b'\n')
self.tn.read_until(b'Password: ', self.timeout)
self.tn.write(self.password.encode() + b'\n')
def send_command(self, command):
self.tn.write(command.encode() + b'\n')
time.sleep(1)
return self.tn.read_very_eager().decode('utf-8', errors='ignore')
def close(self):
if self.tn:
self.tn.close()
legacy = telnet_driver('192.168.1.99', 'admin', 'secret')
legacy.connect()
print(legacy.send_command('show version'))
legacy.close()
Practice Safely — Watch the Plaintext
This one doesn't need a simulated transport — it's a real socket connection, just confined to your own machine (127.0.0.1) instead of a real switch. A tiny background thread plays the part of the legacy device.
import socket
import threading
def fake_legacy_switch(server_socket, ready_event):
"""A toy server that mimics a legacy device's Telnet-style login prompt."""
server_socket.listen(1)
ready_event.set()
conn, _ = server_socket.accept()
conn.sendall(b'Username: ')
conn.recv(1024)
conn.sendall(b'Password: ')
conn.recv(1024)
conn.sendall(b'switch1> ')
conn.close()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 0))
host, port = server.getsockname()
ready = threading.Event()
server_thread = threading.Thread(target=fake_legacy_switch, args=(server, ready))
server_thread.start()
ready.wait(timeout=2)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))
print('Server:', client.recv(1024).decode())
client.sendall(b'admin\n')
print('Server:', client.recv(1024).decode())
client.sendall(b'hunter2\n')
print('Server:', client.recv(1024).decode())
client.close()
server_thread.join(timeout=2)
server.close()
print()
print("Notice 'admin' and 'hunter2' traveled as plain, readable bytes.")
print('Anyone capturing this traffic (Wireshark, a switch port mirror, a')
print('compromised hop) reads the username and password directly.')
print('This is exactly what SSH/Paramiko encrypts -- and Telnet never did.')
📝 Exercise: Wrap It in a Driver Class
Rebuild the same demo using a legacy_driver class with connect(), send_command(), and close() -- the exact same shape as every other driver in this chapter -- so the only thing that changes between Telnet and Paramiko is what's happening on the wire underneath.