Telnet — Why You'll (Almost) Never Use It

15 min In Progress

Learning 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.

Python's telnetlib has been deprecated since Python 3.11 and is slated for removal in 3.13 — the standard library itself is moving away from it.

The Real Driver (Lab / Real-Device Code)

The shape is the same connect / send_command / close pattern as every other driver in this chapter.

Lab-only: needs a real reachable legacy device. Running it here will hit a connection error or time out after 5 seconds — expected, since 192.168.1.99 doesn't exist in this sandbox.
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.

code.py
Output

            
← Back to Chapter