Категория > Новости > Reverse shell на Python. Осваиваем навыки работы с сетью на Python на примере обратного шелла - «Новости»

Reverse shell на Python. Осваиваем навыки работы с сетью на Python на примере обратного шелла - «Новости»


14-04-2020, 20:03. Автор: Агнеса
В этой статье мы разберемся, как при помощи Python передавать сообщения между двумя компьютерами, подключенными к сети. Эта задача часто встречается не только при разработке приложений, но и при пентесте или участии в CTF. Проникнув на чужую машину, мы как-то должны передавать ей команды. Именно для этого нужен reverse shell, или «обратный шелл», который мы и напишем.

Существует два низкоуровневых протокола, по которым передаются данные в компьютерных сетях, — это UDP (User Datagram Protocol) и TCP (Transmission Control Protocol). Работа с ними слегка различается, поэтому рассмотрим оба.


Протокол UDP предназначен для передачи пакетов от одного узла к другому без гарантии доставки. Пакет данных обычно состоит из двух частей. В одной — управляющая информация, в том числе данные отправителя и получателя, а также коды ошибок. В другой — пользовательская информация, то есть сообщение, которое передается.


Важно здесь то, что UDP не гарантирует доставку пакета (правильнее говоря, датаграммы) указанному узлу, так как между отправителем и получателем не установлен канал связи. Ошибки и потери пакетов в сетях случаются сплошь и рядом, и это стоит учитывать (или в определенных случаях, наоборот, не стоит).


Протокол TCP тоже доставляет сообщения, но при этом гарантирует, что пакет долетит до получателя целым и невредимым.


Переходим к практике


Писать код мы будем на современном Python 3. Вместе с Python поставляется набор стандартных библиотек, из которого нам потребуется модуль socket. Подключаем его.


import socket

Дальше мы договоримся, что у нас есть сервер и клиент, где клиентом обычно будет наш компьютер, а сервером — удаленный. В реальности все это условности, и речь может идти о любых двух компьютерах (в том числе виртуальных машинах) или даже просто двух процессах, запущенных локально. Важно только то, что код по разные стороны будет разным.


На каждой из сторон первым делом создаем экземпляр класса socket и устанавливаем для него две константы (параметры).


Используем UDP


Сначала создадим место для обмена данными.


s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

Мы создали объект s, который является экземпляром класса socket. Для этого РјС‹ вызвали метод РёР· модуля socket СЃ именем socket и передали ему два параметра — AF_INET Рё SOCK_DGRAMM. AF_INET означает, что используется IP-протокол четвертой версии. РџСЂРё желании можно использовать IPv6. Р’Рѕ втором параметре для наших целей РјС‹ можем указать РѕРґРЅСѓ РёР· РґРІСѓС… констант: SOCK_DGRAMM или SOCK_STREAM. Первая означает, что будет использоваться протокол UDP. Вторая — TCP.


 

Сторона сервера


Далее код различается для стороны сервера и клиента. Рассмотрим сначала сторону сервера.


s.bind(('127.0.0.1', 8888))
result = s.recv(1024)
print('Message:', result.decode('utf-8'))
s.close()

Здесь s.bind(('127.0.0.1', 8888)) означает, что РјС‹ резервируем РЅР° сервере (то есть РЅР° нашей же машине) адрес 127.0.0.1 Рё РїРѕСЂС‚ 8888. РќР° нем РјС‹ будем слушать Рё принимать пакеты информации. Здесь стоят двойные СЃРєРѕР±РєРё, так как методу bind() передается кортеж данных — в нашем случае состоящий из строки с адресом и номера порта.


Reverse shell на Python. Осваиваем навыки работы с сетью на Python на примере обратного шелла - «Новости»
INFOРезервировать можно только свободные порты. Например, если на порте 80 уже работает веб-сервер, то он будет нам мешать.

Далее метод recv() объекта s прослушивает указанный нами порт (8888) и получает данные по одному килобайту (поэтому мы задаем размер буфера 1024 байта). Если на него присылают датаграмму, то метод считывает указанное количество байтов и они попадают в переменную result.


Далее идет всем знакомая функция print(), в которой мы выводим сообщение Message: Рё декодированный текст. Поскольку данные РІ result — это текст в кодировке UTF-8, мы должны интерпретировать его, вызвав метод decode('utf-8').


РќСѓ Рё наконец, вызов метода close() необходим, чтобы остановить прослушивание 8888-го порта и освободить его.


Таким образом, сторона сервера имеет следующий вид:


import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('127.0.0.1', 8888))
result = s.recv(1024)
print('Message:', result.decode('utf-8'))
s.close()

Сторона клиента


Здесь все гораздо проще. Для отправки датаграммы мы используем метод класса socket (точнее, нашего экземпляра s) под названием .sendto():


s.sendto(b'<Your message>', ('127.0.0.1', 8888))

У метода есть два параметра. Первый — сообщение, которое ты отправляешь. Буква b перед текстом нужна, чтобы преобразовать символы текста в последовательность байтов. Второй параметр — кортеж, где указаны IP машины-получателя и порт, который принимает датаграмму.


Таким образом, сторона клиента будет выглядеть примерно так:


import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b'<Your message>', ('127.0.0.1', 8888))

Тестируем


Для тестирования открываем две консоли, одна у нас будет работать сервером, другая — клиентом. В каждой запускаем соответствующую программу.


Вывод на стороне сервера

На стороне клиента мы ничего увидеть не должны, и это логично, потому что мы ничего и не просили выводить.


Для теста мы передавали сообщение от одного порта другому порту на нашей же машине, но если запустить эти скрипты на разных компьютерах и на стороне клиента указать правильный IP, то все будет работать точно так же.


Используем TCP


Пришло время познакомится с TCP. Точно так же создаем класс s, РЅРѕ РІ качестве второго параметра будем использовать константу SOCK_STREAM.


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Сторона сервера


Снова резервируем порт, на котором будем принимать пакеты:


s.bind(('127.0.0.1', 8888))

Дальше появляется незнакомый нам ранее метод listen(). РЎ его помощью РјС‹ устанавливаем некую очередь для подключенных клиентов. Например, СЃ параметром .listen(5) мы создаем ограничение на пять подключенных и ожидающих ответа клиентов.


Делаем бесконечный цикл, в котором будем обрабатывать запросы от каждого нового клиента, находящегося в очереди.


while 1:
try:
client, addr = s.accept()
except KeyboardInterrupt:
s.close()
break
else:
result = client.recv(1024)
print('Message:', result.decode('utf-8'))

Страшновато? Начнем по порядку. Сначала мы создаем обработчик исключения KeyboardInterrupt (остановка работы программы СЃ клавиатуры), чтобы сервер работал бесконечно, РїРѕРєР° РјС‹ что-РЅРёР±СѓРґСЊ РЅРµ нажмем.


Метод accept() возвращает пару значений, которую мы помещаем в две переменные: в addr Р±СѓРґСѓС‚ содержаться данные Рѕ том, кто был отправителем, Р° client станет экземпляром класса socket. РўРѕ есть РјС‹ создали РЅРѕРІРѕРµ подключение.


Теперь посмотрим вот на эти три строчки:


except KeyboardInterrupt:
s.close()
break

В них мы останавливаем прослушивание и освобождаем порт, только если сами остановим работу программы. Если прерывания не произошло, то выполняется блок else:


else:
result = client.recv(1024)
print('Message:', result.decode('utf-8'))

Здесь мы сохраняем пользовательские данные в переменную result, а функцией print() выводим РЅР° экран сообщение, которое нам отправлял клиент (предварительно превратив байты РІ строку Unicode). Р’ результате сторона сервера будет выглядеть примерно так:


import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8888))
s.listen(5)
while 1:
try:
client, addr = s.accept()
except KeyboardInterrupt:
s.close()
break
else:
result = client.recv(1024)
print('Message:', result.decode('utf-8'))

Сторона клиента


Со стороной клиента опять же все обстоит проще. После подключения библиотеки и создания экземпляра класса s РјС‹, используя метод connect(), подключаемся к серверу и порту, на котором принимаются сообщения:


s.connect(('127.0.0.1', 8888))

Далее мы отправляем пакет данных получателю методом send():


s.send(b'<YOUR MESSAGE>')

В конце останавливаем прослушивание и освобождаем порт:


s.close()

Код клиента будет выглядеть примерно так:


import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 8888))
s.send(b'<YOUR MESSAGE>')
s.close()

Тестируем


Запустим в двух разных консолях код сервера и код клиента. На выходе мы должны получить примерно то же самое, что и с протоколом UDP.


Вывод на стороне сервера

Успех! Поздравляю: теперь тебе открыты большие возможности. Как видишь, ничего страшного в работе с сетью нет. И конечно, не забываем, что раз мы эксперты в ИБ, то можем добавить шифрование в наш протокол.


В качестве следующего упражнения можешь попробовать, например, написать чат на несколько персон, как на скриншоте.


Самодельный чат, вид со стороны сервера

Применяем знания на практике


Я дважды участвовал в InnoCTF, и работа с сокетами в Python очень пригождается при решении задач на взлом. По сути, все сводится к тому, чтобы очень много раз парсить поступающие данные с сервера InnoCTF и правильно их обрабатывать. Данные могут абсолютно любыми. Обычно это математические примеры, разные уравнения и прочее.


Для работы с сервером я использую следующий код.


import socket
try:
s = socket.socket(socket.AF_INET, spcket.SOCK_STREAM)
s.connect(('', ))
while True:
data = s.recv(4096)
if not dаta:
continue
st = data.decode("ascii")
# Здесь идет алгоритм обработки задачи, результаты работы которого должны оказаться в переменной result
s.send(str(result)+'n'.encode('utf-8'))
finally:
s.close()
[/code]

Здесь мы сохраняем байтовые данные в переменную data, Р° потом преобразуем РёС… РёР· РєРѕРґРёСЂРѕРІРєРё ASCII РІ строчке st = data.decode("ascii"). Теперь в переменной st у нас хранится то, что нам прислал сервер. Отправлять ответ мы можем, только подав на вход строковую переменную, поэтому обязательно используем функцию str(). В конце у нее символ переноса строки — n. Далее мы все кодируем в UTF-8 и методом send() отправляем серверу. Р’ конце нам обязательно нужно закрыть соединение.


 

Делаем полноценный reverse shell


От обучающих примеров переходим к реальной задаче — разработке обратного шелла, который позволит выполнять команды на захваченной удаленной машине.


РџСЂРё этом добавить нам нужно только вызов функции subprocess. Что это такое? В Python есть модуль subprocess, который позволяет запускать в операционной системе процессы, управлять ими и взаимодействовать с ними через стандартный ввод и вывод. В качестве простейшего примера используем subprocess, чтобы запустить блокнот:


import subprocess
subprocess.call('notepad.exe')

Здесь метод call() вызывает (запускает) указанную программу.


Переходим к разработке шелла. В данном случае сторона сервера будет атакующей (то есть наш компьютер), а сторона клиента — атакованной машиной. Именно поэтому шелл называется обратным.



Перейти обратно к новости