You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
192 lines
5.8 KiB
192 lines
5.8 KiB
// (c) 2020 Taeyeon Mori |
|
// |
|
|
|
#include "koutil.hpp" |
|
#include "kofs.hpp" |
|
#include "kofd.hpp" |
|
#include "koos.hpp" |
|
|
|
#include <cstdlib> |
|
#include <sstream> |
|
#include <iostream> |
|
#include <filesystem> |
|
|
|
#include <unistd.h> |
|
#include <pwd.h> |
|
#include <mntent.h> |
|
|
|
|
|
struct params { |
|
std::filesystem::path motd; |
|
bool ro = true; |
|
bool protect = true; |
|
char *const *argv = nullptr; |
|
}; |
|
|
|
void usage(const char *prog) { |
|
std::cout << "Usage: " << prog << " [-m MOTD] [ARGV...]" << std::endl |
|
<< std::endl |
|
<< "Options:" << std::endl |
|
<< " -m MOTD Specify a file to be displayed on login" << std::endl |
|
<< " ARGV Specify the shell executable and arguments" << std::endl |
|
<< " By default, the shell from /etc/passwd is used with argument -l" << std::endl |
|
; |
|
} |
|
|
|
params parse_args(int argc, char **argv) { |
|
params p{}; |
|
|
|
constexpr auto spec = "+hm:"; |
|
|
|
while (true) { |
|
auto opt = getopt(argc, const_cast<char *const *>(argv), spec); |
|
|
|
if (opt == -1) |
|
break; |
|
else if (opt == '?' || opt == 'h') { |
|
usage(argv[0]); |
|
exit(opt == 'h' ? 0 : 1); |
|
} else if (opt == 'm') { |
|
p.motd = ::optarg; |
|
} |
|
} |
|
|
|
if (argc > ::optind) |
|
p.argv = const_cast<char *const *>(&argv[::optind]); |
|
|
|
return p; |
|
} |
|
|
|
// Helpers |
|
int xerror(const char *desc) { |
|
perror(desc); |
|
return -errno; |
|
} |
|
|
|
[[noreturn]] void die(int r, const char *msg) { |
|
std::cerr << msg << std::endl; |
|
exit(r); |
|
} |
|
|
|
[[noreturn]] void die_errno(int r, const char *msg) { |
|
perror(msg); |
|
exit(r); |
|
} |
|
|
|
struct mntent_context { |
|
FILE *mounts; |
|
|
|
mntent_context(char const *fname) { |
|
mounts = setmntent(fname, "r"); |
|
} |
|
|
|
operator bool() { |
|
return mounts != nullptr; |
|
} |
|
|
|
~mntent_context() { |
|
if (mounts != nullptr) |
|
endmntent(mounts); |
|
} |
|
|
|
mntent *next() { |
|
return getmntent(mounts); |
|
} |
|
}; |
|
|
|
int ro_all_mounts() { |
|
// Change all current mounts to readonly |
|
auto mounts = mntent_context("/proc/mounts"); |
|
if (!mounts) |
|
return 1; |
|
|
|
mntent *ent; |
|
while ((ent = mounts.next()) != nullptr) { |
|
if (ko::os::bind(ent->mnt_dir, ent->mnt_dir, MS_REMOUNT|MS_RDONLY)) |
|
return 1; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
int protect_self() { |
|
// Hide self by by bind-mounting /dev/zero on top. Make it harder to exploit any vulns, just in case |
|
// Though this does give away the name of the executable... |
|
auto path = ko::fd::readlink("/proc/self/exe"); |
|
if (path.empty()) |
|
return 1; |
|
return ko::os::bind("/dev/null", path); |
|
} |
|
|
|
int main(int argc, char **argv) { |
|
params p = parse_args(argc, argv); |
|
uid_t ruid, euid, suid; |
|
gid_t rgid, egid, sgid; |
|
passwd *passwd; |
|
std::string options; |
|
const char *default_shell_argv[] = {nullptr, "-l", nullptr}; // gets executable name from user passwd record |
|
|
|
// Use shell from passwd if no command is given in argv |
|
if (p.argv == nullptr) |
|
p.argv = const_cast<char *const*>(default_shell_argv); |
|
|
|
auto [e, eloc] = ko::util::cvshort() |
|
.then("getresuid", getresuid, &ruid, &euid, &suid) |
|
.then("getresgid", getresgid, &rgid, &egid, &sgid) |
|
.then("getpwuid", [ruid,euid,&passwd]() { |
|
// Check root perms |
|
if (euid != 0) |
|
die(3, "Must be suid root"); |
|
// Retrieve user info |
|
errno = 0; |
|
passwd = getpwuid(ruid); |
|
if (errno != 0) |
|
return 5; |
|
else if (passwd == nullptr) |
|
die(4, "Calling user ID not known to system"); |
|
return 0; |
|
}) |
|
.then("setegid", ::setegid, 0) |
|
.then("unshare", ::unshare, CLONE_NEWNS) |
|
.then("make ns slave", ko::os::mount, "", "/", "", MS_REC|MS_SLAVE, nullptr) |
|
.ifthen("make fs readonly", p.ro, ro_all_mounts) |
|
.ifthen("protect self", p.protect, protect_self) |
|
.then("mount tmp", ko::os::mount, "tmpfs", "/tmp", "tmpfs", MS_NOEXEC|MS_NODEV|MS_NOSUID, nullptr) |
|
.then([ruid,rgid,suid,&options,passwd,&default_shell_argv]() -> ko::util::cvresult { |
|
// Create directories |
|
auto d = ko::fd::opendir("/tmp"); |
|
auto r = ko::util::cvshort() |
|
.then("fchown tmp", ::fchown, (int)d, ruid, rgid) |
|
.then("setegid", ::setegid, rgid) |
|
.then("seteuid", ::seteuid, ruid) |
|
.then("mkdir .home", ko::fd::mkdir, ".home", 0750, (int)d) |
|
.then("mkdir work", ko::fd::mkdir, ".home/work", 0750, (int)d) |
|
.then("mkdir top", ko::fd::mkdir, ".home/top", 0750, (int)d) |
|
.then("seteuid root", ::seteuid, suid); |
|
if (r) { |
|
// Build option string |
|
options = ko::util::str("lowerdir=", passwd->pw_dir, ",upperdir=/tmp/.home/top,workdir=/tmp/.home/work"); |
|
// Use shell from passwd |
|
default_shell_argv[0] = passwd->pw_shell; |
|
} |
|
return r; |
|
}) |
|
.then("mount overlay", ko::os::mount, "overlay", passwd->pw_dir, "overlay", 0, (void*)options.c_str()) |
|
.ifthen("show motd", !p.motd.empty(), [&p]() { |
|
auto f = ko::fd::open(p.motd, O_RDONLY); |
|
if (!f) |
|
return 1; |
|
struct stat st; |
|
if (::fstat(f, &st)) |
|
return 1; |
|
return ko::fd::fcopy(f, STDOUT_FILENO, st.st_size) ? 0 : 1; |
|
}) |
|
.then("chdir home", ::chdir, passwd->pw_dir) |
|
.then("drop gid", ::setresgid, rgid, rgid, rgid) |
|
.then("drop uid", ::setresuid, ruid, ruid, ruid) |
|
.then("exec", ::execvp, p.argv[0], p.argv); |
|
|
|
perror(eloc); |
|
return e; |
|
} |
|
|
|
|