From df3d81128887209e083218bf1e3942d13df2e57e Mon Sep 17 00:00:00 2001 From: bbergeron Date: Mon, 29 Apr 2024 18:53:03 -0400 Subject: Reset git history with pseudonym --- .gitignore | 1 + LICENSE | 21 +++++++++++ Makefile | 29 +++++++++++++++ README.md | 11 ++++++ main.c | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ mntrun.1 | 65 ++++++++++++++++++++++++++++++++ mount.c | 36 ++++++++++++++++++ 7 files changed, 287 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 main.c create mode 100644 mntrun.1 create mode 100644 mount.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66aa312 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +mntrun diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d31b869 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT/X Consortium License + +© 2024 B. Bergeron + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9482f1a --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +# See LICENSE file for copyright and license details. +.POSIX: + +VERSION ?= 0.1 + +PREFIX ?= /usr/local +MANPREFIX ?= $(PREFIX)/share/man + +CFLAGS = -Wall -pedantic -DMNTRUN_VERSION=\"$(VERSION)\" -std=c99 + +mntrun: main.c mount.c + $(CC) $(CFLAGS) main.c -o mntrun + +clean: + rm mntrun + +install: mntrun + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f mntrun $(DESTDIR)$(PREFIX)/bin + chmod 4755 $(DESTDIR)$(PREFIX)/bin/mntrun + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + sed "s/VERSION/$(VERSION)/g" < mntrun.1 > $(DESTDIR)$(MANPREFIX)/man1/mntrun.1 + chmod 644 $(DESTDIR)$(MANPREFIX)/man1/mntrun.1 + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/mntrun + rm -f $(DESTDIR)$(MANPREFIX)/man1/mntrun.1 + +.PHONY: clean install uninstall diff --git a/README.md b/README.md new file mode 100644 index 0000000..152ea64 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +mntrun - mounts binds and overlays inside an ephemeral mount namespace, and runs a command in it. + +Usage: `mntrun [-dhv] [-b source dest] [-o|-m lower upper workdir mountpoint] command` + +This tiny program exploits SUID to allow regular users to create overlays and binds mounts inside an ephemeral namespace, and to run `command` inside this new namespace. + +For further usage documentation, consult the dedicated man page. + +mntrun is fewer than 200 lines of fairly simple C99. `mount.c` contains all (2) mount wrappers, while `main.c` contains `main` and `namespace_main`, which does what you expect. + +glibc Linux only (`clone(2)` seems to be non-standard, sorry) \ No newline at end of file diff --git a/main.c b/main.c new file mode 100644 index 0000000..7cb565b --- /dev/null +++ b/main.c @@ -0,0 +1,124 @@ +/* See LICENSE file for copyright and license details. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include "mount.c" + +#ifndef MNTRUN_VERSION +#define MNTRUN_VERSION "(unknown)" +#endif + +#define USAGE "usage: mntrun [-dhv] [-b source dest] [[-o|-m] upperdir lowerdir workdir mountpoint] command\n" + +static inline int checkflag(char *arg, char f) { + return arg && (arg[0] == '-' && arg[1] == f && arg[2] == '\0'); +} + +int namespace_main(void *data) { + uid_t uid = getuid(); + gid_t gid = getgid(); + + int verbose = 0; + char **argv = data; + + if(checkflag(argv[0], 'h')) { + fputs(USAGE, stderr); + return EXIT_SUCCESS; + } + + if(checkflag(argv[0], 'v')) { + puts("mntrun " MNTRUN_VERSION); + return EXIT_SUCCESS; + } + + if(checkflag(argv[0], 'd')) { + verbose = 1; + argv ++; + } + + /* Jump back here over-and-over until all mount directives are parsed. */ + do_mounts: + if(checkflag(argv[0], 'b')) { + if(!argv[1] || !argv[2]) goto exit_usage; + + if(mount_bind(argv[1], argv[2]) == -1) { + if(errno) fprintf(stderr, "Failed to bind '%s' to '%s': %s\n", argv[1], argv[2], strerror(errno)); + return EXIT_FAILURE; + } + argv += 3; + goto do_mounts; + } + + int merge = 0; + if(checkflag(argv[0], 'o') || (merge = checkflag(argv[0], 'm'))) { + char *type_str = merge ? "merge" : "overlay"; + + if(!argv[1] || !argv[2] || !argv[3] || !argv[4]) goto exit_usage; + if(verbose) printf("[%s]\n\tlower=%s\n\tupper=%s\n\twork=%s\n\tmount=%s\n", type_str, argv[1], argv[2], argv[3], argv[4]); + + char *lowers = argv[1]; + if(merge) { + lowers = malloc(strlen(argv[4]) + 1 + strlen(argv[1])); + sprintf(lowers, "%s:%s", argv[4], argv[1]); + } + + if(mount_overlay(uid, gid, lowers, argv[2], argv[3], argv[4]) == -1) { + if(errno) fprintf(stderr, "Failed to mount %s on '%s': %s\n", type_str, argv[4], strerror(errno)); + return EXIT_FAILURE; + } + argv += 5; + if(merge) { + free(lowers); + merge = 0; + } + goto do_mounts; + } + /* Check if a command was specified */ + if(!argv[0]) goto exit_usage; + + setuid(uid); + + /* These next lines take the remaining values in `argv` and join them with spaces in `command`. */ + int charc; + for(int i = 0; argv[i]; i ++) charc += strlen(argv[i]); + char *command = malloc(charc + 1); + command[0] = '\0'; + for(int i = 0; argv[i]; i ++) { + strcat(command, " "); + strcat(command, argv[i]); + } + + int status = system(command); + if(status == -1) { + perror("system"); + return EXIT_FAILURE; + } + return status; + +exit_usage: + fputs(USAGE, stderr); + return EXIT_FAILURE; +} + +int main(int argc, char **argv) { + #define STACK_SIZE 1024 * 1024 + char *stack = malloc(STACK_SIZE); + pid_t pid = clone(namespace_main, stack + STACK_SIZE, CLONE_NEWNS | SIGCHLD, argv + 1); + if(pid == -1) { + perror("Failed to create a new mount namespace"); + return EXIT_FAILURE; + } + + int wstatus; + if (waitpid(pid, &wstatus, 0) == -1) { + /* The only error which may occurs is EINTR, but I'm not 100% sure. */ + perror("waitpid"); + return EXIT_FAILURE; + } + return WEXITSTATUS(wstatus); +} diff --git a/mntrun.1 b/mntrun.1 new file mode 100644 index 0000000..b908c50 --- /dev/null +++ b/mntrun.1 @@ -0,0 +1,65 @@ +.TH USERMNT 1 mntrun\-VERSION +.SH NAME +mntrun \- mounts binds and overlays inside an ephemeral mount namespace, and runs a command in it +.SH SYNOPSIS +.B mntrun +.RB [ \-dhv ] +.RB [ \-b +.IR "source dest" ] +.RB [[ \-o | \-m ] +.IR "lower upper workdir mountpoint" ] +.IR command +.SH DESCRIPTION +Exploits SUID to allow regular users to create overlays and binds mounts inside an ephemeral namespace, and to run +.I command +inside this new namespace. mntrun prints to stderr whenever an error occurs, but errors concerning mounts are better diagnosed using +.BR dmesg (1). +.TP +These switches are mutually exclusive and must come right after 'mntrun': +.TP +.B \-d +Enable debug logs. +.TP +.B \-h +Display usage and quit. +.TP +.B \-v +Display version and quit. +.TP +The following mount directives can appear several times: +.TP +.BI \-b " source dest" +Bind: binds +.I source +to +.IR dest . +This directive will fail if the current user does't have the permission to write to +.IR dest . +.TP +.BI \-o " lowers upper workdir mountpoint" +Overlay: overlays +.IR lowers , +a colon-separated list of read-only directories, and a writable directory +.IR upper +onto +.IR mountpoint , +using +.I workdir +as the working directory. This directive will fail if the current user doesn't have the permission to write to upper, workdir or mountpoint . +.TP +.BI \-m " lowers upper workir mountpoint" +Merge: like +.BR \-o , +but +.I mountpoint +is prepended to +.IR lowers . +.TP +As mentioned, mntrun must belong to root and have SUID permission to work for regular users. + +.SH AUTHORS +B. Bergeron + +.SH SEE ALSO +.BR mount (8), +.BR dmesg (1) diff --git a/mount.c b/mount.c new file mode 100644 index 0000000..b4a6150 --- /dev/null +++ b/mount.c @@ -0,0 +1,36 @@ +/* See LICENSE file for copyright and license details. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +int mount_overlay(uid_t uid, gid_t gid, const char *restrict lower, const char *restrict upper, const char *restrict workdir, const char *restrict mountpoint) { + if(access(mountpoint, W_OK) || access(upper, W_OK) || access(workdir, W_OK)) return -1; + + char *options = malloc( + sizeof("lowerdir=") + strlen(lower) + + sizeof(",upperdir=") + strlen(upper) + + sizeof(",workdir=") + strlen(workdir) + ); + sprintf(options, "lowerdir=%s,upperdir=%s,workdir=%s", lower, upper, workdir); + int mount_status = mount("overlay", mountpoint, "overlay", 0, options); + free(options); + if(mount_status) return -1; + + /* When mounting an overlay, a folder named 'work' belonging to root:root is created inside workdirt. These next lines give the ownership back to the current user. */ + char *other_work_dir = malloc(strlen(workdir) + sizeof("/work")); + sprintf(other_work_dir, "%s/work", workdir); + int status = chown(other_work_dir, uid, gid); + free(other_work_dir); + return status; +} + +int mount_bind(const char *restrict source, const char *restrict dest) { + if(access(dest, W_OK)) return -1; + return mount(source, dest, "none", MS_BIND, NULL); +} -- cgit v1.2.3