Подделка ssh сервера на Python
Спойлеры
В этом посте я расскажу, как игрался и создавал на python сокеты, к которым можно подключаться через ssh клиенты.
Для начала (если оно не сломается добрыми людьми) можно попробовать подключиться к примеру:
ssh root@ssh.vsecoder.dev -p 2200
пароль habr
Это стена «Был здесь», поле ввода принимает ник, сохраняет, и потом выводит другим.
Предисловие
Что такое ssh уже хорошо рассказал один автор на хабре: https://habr.com/ru/articles/122445/
Я лишь покажу красивую схемку от рег.ру:
принцип работы ssh
И скажу что мы заменим SSH-сервер своим скриптом на Python, а за tcp соединение будет отвечать модуль socket.
Начнём
Socket
— встроенный модуль для создания низкоуровневых сетевых интерфейсов. Он включает в себя функции создания объекта сокета Socket
, который и обрабатывает канал данных, а также функции, связанных с сетевыми задачами, такими как преобразование имени сервера в IP адрес и форматирование данных для отправки по сети.
Сокеты можно настроить для работы в качестве сервера и прослушивания входящих сообщений или для подключения к другим приложениям в качестве клиента. После подключения обоих концов сокета TCP/IP обмен данными становится двунаправленным.
В пример поднимем простой сервер и клиент:
Подробнее про этот пример: https://docs-python.ru/standart-library/modul-socket-setevoj-interfejs-python/
# сервер
import socket
import sys
# создаем TCP/IP сокет
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Привязываем сокет к порту
server_address = ('localhost', 1234)
sock.bind(server_address)
# Слушаем входящие подключения
sock.listen(1)
while True:
# ждем соединения
connection, client_address = sock.accept()
try:
# Принимаем данные порциями и ретранслируем их
while True:
data = connection.recv(16)
if data:
data = data.upper()
print(data)
connection.sendall(data)
else:
break
finally:
# Очищаем соединение
connection.close()
# клиент
import socket
import sys
# Создаем TCP/IP сокет
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Подключаем сокет к порту, через который прослушивается сервер
server_address = ('localhost', 10000)
print('Подключено к {} порт {}'.format(*server_address))
sock.connect(server_address)
try:
# Отправка данных
mess = 'Hello Wоrld!'
print(f'Отправка: {mess}')
message = mess.encode()
sock.sendall(message)
# Смотрим ответ
amount_received = 0
amount_expected = len(message)
while amount_received < amount_expected:
data = sock.recv(16)
amount_received += len(data)
mess = data.decode()
print(f'Получено: {data.decode()}')
finally:
sock.close()
Но в нашем случае нам придётся писать только серверный сокет, т.к. в роли клиента у нас выступает ssh клиент.
Вроде всё легко, подними сокет, да общайся, но не всё так однозначно. Попробуем создать сокет и подключиться к нему:
данные от клиента ssh
Клиент хочет с нами пообщаться, и если просто попробовать слать ему не то, что он ожидает, клиент принудительно разорвёт соединение:
Connection reset by 127.0.0.1 port 1234
Конечно можно попробовать со всем этим разобраться ручками, но я нашёл другой модуль, решающий эту проблему — paramiko
.
Он позволяет как создавать ssh клиентские подключения, так и сервера. На гитхабе у него уже есть несколько готовых примеров, так и один с созданием сервера, но мне он показался перегруженным, поэтому покажу свой:
import socket
import paramiko
import threading
from paramiko import Transport, RSAKey
# ssh-keygen
good_pub_key = RSAKey(filename="test.rsa")
class Server(paramiko.ServerInterface):
def __init__(self):
self.event = threading.Event()
self.user = 'root'
self.password = 'root'
def check_channel_request(self, kind, chanid):
if kind == "session":
return paramiko.OPEN_SUCCEEDED
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
def check_auth_password(self, username, password):
if (username == self.user) and (password == self.password):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
def check_auth_publickey(self, username, key):
if (username == self.user) and (key == good_pub_key):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
def enable_auth_gssapi(self):
return False
def get_allowed_auths(self, username):
return "password,publickey"
def check_channel_shell_request(self, channel):
self.event.set()
return True
def check_channel_pty_request(
self, channel, term, width, height, pixelwidth, pixelheight, modes
):
return True
if __name__ == "__main__":
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('', 2200))
server.listen(5)
while True:
try:
client, addr = server.accept()
t = Transport(client)
t.load_server_moduli()
t.add_server_key(good_pub_key)
t.start_server(server=Server())
chan = t.accept(20)
if not chan:
continue
chan.send(
"Welcome to my test server!\n \r"
)
chan.send("\n \n \r")
chan.close()
except Exception as e:
print(e)
В примере я создаю серверный интерфейс (класс на строке №10), на основе которого будет создана сессия подключения, аутентификация и тд.
Для запуска примера необходимо создать публичный ключ командой ssh-keygen и указать на 7й строчке путь к нему. Вот что будет, если подключиться к этому серверу:
> ssh root@localhost -p 2200
root@localhost's password:
Permission denied, please try again.
root@localhost's password:
Welcome to my test server!
Connection to localhost closed.
Для подключения нам нужно указать пользователя и пароль, так же paramiko поддерживает закрытые ключи, и, вроде должна быть возможность пропустить аутентификацию, но я такого не нашёл.
Что дальше? Дальше можно:
Прикалываться, представьте реакцию, если человек подключится к серверу и получит подобное (но не переборщите, для запуска можно поменять порт ssh, а сокет запустить на 22 м порте):
Устраивать опросы или создать свою ASCII галерею:
Да и просто интересная практика для изучения сокетов, tcp подключений и тд.
Полезная литература:
https://docs-python.ru/standart-library/modul-socket-setevoj-interfejs-python/
https://help.reg.ru/support/hosting/dostupy-i-podklyucheniye-panel-upravleniya-ftp-ssh/chto-takoye-ssh
https://www.paramiko.org