April 25

Ред-флаг

CTF: АльфаЦТФ2026

Автор: @m0nr0e21

Категория: Linux / Logic / Bash
Сложность: Easy
Суть: ошибка бизнес-логики при присоединении к тир-листу + некорректная проверка прав доступа к вложенным файлам.


Описание задачи

Дан SSH-доступ к консольному сервису на bash, который реализует тир-листы через whiptail.

Пользователь может:

  • выбрать имя пользователя;
  • просматривать публичные тир-листы;
  • создавать свои тир-листы;
  • присоединяться к чужим;
  • добавлять/удалять тиры и элементы;
  • просматривать элементы, если они не ограничены владельцем.

Флаг спрятан в элементе flag, который отображается как:

text[LOCKED] flag

При попытке открыть его сервис сообщает:

textДоступ ограничен владельцем тир-листа.

Анализ исходного кода

Главные данные хранятся в директории:

bashTIERLISTS_DIR="/data/tierlists"

Каждый тир-лист — это директория с .meta:

textowner=<user>
restricted=<file1,file2,...>
collaborators=<user1,user2,...>

Проверка владельца:

bashis_owner() {
    local owner
    owner=$(meta_get "$1" "owner") || return 1
    [[ "$owner" == "$CURRENT_USER" ]]
}

Проверка ограничения доступа:

bashis_restricted() {
    local restricted
    restricted=$(meta_get "$1" "restricted") || return 1
    [[ -z "$restricted" ]] && return 1
    local IFS=','
    for r in $restricted; do
        [[ "$r" == "$2" ]] && return 0
    done
    return 1
}

Чтение файла разрешено так:

bashcan_read_file() {
    if is_restricted "$1" "$2"; then
        is_owner "$1"
    else
        return 0
    fi
}

Если имя файла есть в restricted, читать его может только владелец тир-листа.


Уязвимость №1: небезопасный join

При присоединении к чужому тир-листу вызывается функция:

bashjoin_tierlist_by_name() {
    local tl_name="$1"
    local new_name="${tl_name}__with__${CURRENT_USER}"

    mv "$TIERLISTS_DIR/$tl_name" "$TIERLISTS_DIR/$new_name"

    local collabs
    collabs=$(meta_get "$TIERLISTS_DIR/$new_name" "collaborators") || collabs=""
    if [[ -z "$collabs" ]]; then
        meta_set "$TIERLISTS_DIR/$new_name" "collaborators" "$CURRENT_USER"
    else
        meta_set "$TIERLISTS_DIR/$new_name" "collaborators" "${collabs},${CURRENT_USER}"
    fi

    msg "Вы присоединились к тир-листу."
}

Проблемная строка:

bashmv "$TIERLISTS_DIR/$tl_name" "$TIERLISTS_DIR/$new_name"

Если директория назначения уже существует, mv не перезаписывает её, а перемещает исходную директорию внутрь неё.

Например, если существует:

text/data/tierlists/gorpcore_redflags__with__pwn2

и мы присоединяемся к:

text/data/tierlists/gorpcore_redflags

то команда:

bashmv /data/tierlists/gorpcore_redflags /data/tierlists/gorpcore_redflags__with__pwn2

превратит структуру в:

text/data/tierlists/gorpcore_redflags__with__pwn2/
├── .meta
└── gorpcore_redflags/
    ├── .meta
    └── tier_d/
        └── flag.txt

Внешняя директория остаётся нашей, а чужой тир-лист оказывается вложенным внутрь.


Уязвимость №2: рекурсивный поиск тиров

При отображении тир-листа сервис ищет директории с тирами так:

bashfind "$tl_dir" -type d -name 'tier_*' -print0 2>/dev/null | sort -z

Используется рекурсивный поиск. После предыдущего бага сервис начинает находить не только наши тиры, но и тиры вложенного чужого тир-листа:

textgorpcore_redflags/tier_d

Элементы из этих вложенных директорий попадают в меню.


Уязвимость №3: проверка доступа идёт по неправильной .meta

Когда пользователь выбирает элемент, вызывается:

bashview_item "$tl_dir" "$item_path"

Внутри view_item проверка доступа выглядит так:

bashif ! can_read_file "$tl_dir" "$item_fname"; then
    msg "[LOCKED] $item_name\\n\\nДоступ ограничен владельцем тир-листа."
    return
fi

can_read_file получает именно внешний $tl_dir. То есть если мы открываем файл:

text/data/tierlists/gorpcore_redflags__with__pwn2/gorpcore_redflags/tier_d/flag.txt

проверка ограничений всё равно смотрит в:

text/data/tierlists/gorpcore_redflags__with__pwn2/.meta

А это наш тир-лист, где:

textowner=pwn2
restricted=
collaborators=

Поскольку restricted пустой, файл считается доступным — даже если во вложенном админском тир-листе он был restricted.


Эксплуатация

В публичных тир-листах был найден интересный список:

textgorpcore_redflags @admin

В нём присутствовал закрытый элемент:

text[LOCKED] flag

Краткая последовательность действий:

  1. Подключиться по SSH.
  2. Войти под новым пользователем, например pwn2.
  3. Создать тир-лист с именем gorpcore_redflags__with__pwn2.
    Это имя важно — функция join_tierlist_by_name при присоединении строит имя новой директории именно так: "${tl_name}__with__${CURRENT_USER}".
  4. Открыть публичные тир-листы.
  5. Выбрать gorpcore_redflags @admin.
  6. Нажать «Присоединиться».
    Вместо нормального переименования произойдёт перенос админского тир-листа внутрь директории-ловушки.
  7. Перейти в «Мои тир-листы» и открыть gorpcore_redflags__with__pwn2.
    Если всё сделано правильно, заголовок покажет владельца внешнего тир-листа:textgorpcore_redflags__with__pwn2 [@pwn2]А внутри станут видны тиры и элементы из вложенного gorpcore_redflags.
  8. Открыть элемент [LOCKED] flag.
    Сервис откроет его содержимое, потому что проверяет ограничения по нашей внешней .meta, а не по .meta исходного админского тир-листа.
  9. Получить флаг.

Корневая причина

В задаче объединились три логические ошибки:

  1. join_tierlist_by_name использует mv без проверки существования директории назначения.
  2. view_tierlist рекурсивно ищет tier_* внутри всего дерева тир-листа.
  3. view_item проверяет доступ к файлу по .meta внешней директории, а не по .meta реального тир-листа, которому принадлежит файл.

Вместе это позволяет поместить чужой тир-лист внутрь своего и обойти restricted-проверку.