Skip to content

Instantly share code, notes, and snippets.

@ConnorNelson
Created August 5, 2024 21:35
Show Gist options
  • Select an option

  • Save ConnorNelson/dd69e5bdf5dede11a1b557adb0c39f62 to your computer and use it in GitHub Desktop.

Select an option

Save ConnorNelson/dd69e5bdf5dede11a1b557adb0c39f62 to your computer and use it in GitHub Desktop.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <limits.h>
#include <stdarg.h>
/*
TODO:
We should have the script path as follows:
- Pass it relative exactly as we receive it (if that hierarchy is safe)
- Pass it as a resolved (relative) path (if that hierarchy is safe)
- Pass it as a /proc/self/fd/ link
Let's recursively walk from root node -> link node
For each node, validate that it is owned (both user/group) by the link owner---and not writable by other, unless it is sticky
If we encounter a node where this is not true, we should indicate that the link is not secure, and either pass link_path or fail
Maybe dup2 the fd to 42
*/
#define AT_EMPTY_PATH 0x1000
#define execveat(fd, path, argv, envp, flags) syscall(322, fd, path, argv, envp, flags)
int log_enabled = 0;
void log_info(const char *format, ...) {
if (log_enabled) {
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
}
}
void log_error(const char *format, ...) {
if (log_enabled) {
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
}
exit(1);
}
int main(int argc, char *argv[], char *envp[]) {
log_enabled = getenv("DEBUG_EXEC") != NULL;
char *path;
switch (argc) {
case 3:
path = argv[2];
break;
case 2:
path = argv[1];
break;
default:
fprintf(stderr, "Usage: %s [path]\n", argv[0]);
return 1;
}
int fd = open(path, O_RDONLY);
if (fd == -1)
log_error("Failed to open %s: %s\n", path, strerror(errno));
char link_path[PATH_MAX];
snprintf(link_path, sizeof(link_path), "/proc/self/fd/%d", fd);
char link[PATH_MAX];
ssize_t link_len = readlink(link_path, link, sizeof(link) - 1);
if (link_len == -1)
log_error("Failed to readlink %s: %s\n", link_path, strerror(errno));
link[link_len] = '\0';
log_info("Link: %s\n", link);
char header[1024];
FILE *file = fdopen(fd, "r");
if (fgets(header, sizeof(header), file) == NULL)
log_error("Failed to read header %s: %s\n", path, strerror(errno));
log_info("Header: %s", header);
char *header_cursor = header;
if (strncmp(header, "#!", 2) != 0)
log_error("File %s header does not start with #!\n", path);
if (header[strlen(header) - 1] != '\n')
log_error("File %s header does not end with a newline\n", path);
header_cursor += 2;
while ((*header_cursor == ' ' || *header_cursor == '\t') && *header_cursor != '\n')
header_cursor++;
char *token;
char *exec_argv[256];
char *script_argv[256];
char **argv_cursor = exec_argv;
while ((token = strsep(&header_cursor, " \t\n")) != NULL) {
if (*token == '\0')
continue;
if (strcmp(token, "--") == 0)
break;
*argv_cursor++ = token;
}
*argv_cursor = NULL;
argv_cursor = script_argv;
while ((token = strsep(&header_cursor, " \t\n")) != NULL) {
if (*token == '\0')
continue;
*argv_cursor++ = token;
}
*argv_cursor++ = link_path;
*argv_cursor = NULL;
log_info("Arguments (exec):\n");
for (char **arg = exec_argv; *arg != NULL; arg++)
log_info(" %s\n", *arg);
log_info("Arguments (script):\n");
for (char **arg = script_argv; *arg != NULL; arg++)
log_info(" %s\n", *arg);
// TODO: probably not necessary; remove so we can support /usr/bin/env -S prefixes
if (strcmp(exec_argv[0], argv[0]) != 0)
log_error("Executed binary %s does not match the expected binary %s\n", exec_argv[0], argv[0]);
struct stat file_stat;
if (fstat(fd, &file_stat) == -1)
log_error("Failed to fstat %s: %s\n", path, strerror(errno));
log_info("Owner (UID, GID): %d %d\n", file_stat.st_uid, file_stat.st_gid);
log_info("Permissions: %o\n", file_stat.st_mode & 07777);
FILE *mounts = fopen("/proc/self/mounts", "r");
if (!mounts)
log_error("Failed to open /proc/self/mounts: %s\n", strerror(errno));
char *mount_lines[1024];
int mount_lines_count = 0;
char line[1024];
while (fgets(line, sizeof(line), mounts)) {
mount_lines[mount_lines_count++] = strdup(line);
if (mount_lines_count >= 1024)
log_error("Too many mount points to process\n");
}
fclose(mounts);
char mount_point[PATH_MAX] = "";
char mount_options[1024];
for (int i = mount_lines_count - 1; i >= 0; i--) {
char point[PATH_MAX];
char options[1024];
sscanf(mount_lines[i], "%*s %s %*s %s %*s %*s", point, options);
if (strncmp(link, point, strlen(point)) == 0) {
strncpy(mount_point, point, sizeof(mount_point));
strncpy(mount_options, options, sizeof(mount_options));
break;
}
}
if (mount_point[0] == '\0')
log_error("Failed to find mount point for %s\n", link);
log_info("Mount Point: %s\n", mount_point);
log_info("Mount Options: %s\n", mount_options);
int is_nosuid = 0;
char *mount_option = strtok(mount_options, ",");
while (mount_option != NULL) {
if (strcmp(mount_option, "nosuid") == 0) {
is_nosuid = 1;
break;
}
mount_option = strtok(NULL, ",");
}
int new_euid = file_stat.st_mode & S_ISUID && !is_nosuid ? file_stat.st_uid : getuid();
int new_egid = file_stat.st_mode & S_ISGID && !is_nosuid ? file_stat.st_gid : getgid();
log_info("Setting EUID to %d\n", new_euid);
log_info("Setting EGID to %d\n", new_egid);
if (setegid(new_egid) == -1)
log_error("Failed to setegid %d: %s\n", new_egid, strerror(errno));
if (seteuid(new_euid) == -1)
log_error("Failed to seteuid %d: %s\n", new_euid, strerror(errno));
// int result = execveat(fd, "", script_argv, envp, AT_EMPTY_PATH);
int result = execve(script_argv[0], script_argv, envp);
if (result == -1)
log_error("Failed to execveat %s: %s\n", path, strerror(errno));
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment