python.nix (5254B)
1 # modules/features/pentest/python.nix — one interpreter with every offensive 2 # library importable, plus impacket's 70 example scripts reachable by name. 3 { lib, ... }: 4 (import ./_sets.nix { inherit lib; }) { 5 name = "python"; 6 description = "offensive python: impacket, certipy, pypykatz and friends"; 7 8 packages = pkgs: 9 let 10 inherit (import ./_impacket.nix { inherit lib pkgs; }) impacket aliases; 11 12 # Libraries, for importing. Applications are NOT installed from this env: 13 # python3.withPackages links every package's console scripts into the 14 # env's bin/, so an app that is ALSO installed standalone (certipy in 15 # ad.nix, bloodhound-py, pypykatz…) gives two store paths owning one 16 # name, which is a profile collision that stops the system building. 17 # checks.pentest-collisions found exactly that, twice. 18 env = pkgs.python3.withPackages (ps: with ps; [ 19 impacket 20 certipy 21 dploot 22 masky 23 ldapdomaindump 24 pypykatz 25 bloodyad 26 lsassy 27 minikerberos 28 aiowinreg 29 dnspython 30 scapy 31 pwntools 32 pycryptodomex 33 requests 34 rich 35 ]); 36 37 # Spec C2's discovery command: `impacket` alone lists the 70 scripts 38 # (through fzf when there is a terminal), `impacket <name>` runs one. 39 # Tab-completing `impacket-` does most of this already, but this is the 40 # entry point when you cannot remember whether it is GetUserSPNs or 41 # getuserspns. 42 impacket-cmd = pkgs.writeShellScriptBin "impacket" '' 43 set -uo pipefail 44 names() { 45 ${pkgs.coreutils}/bin/ls -1 ${aliases}/bin \ 46 | ${pkgs.gnugrep}/bin/grep '^impacket-' \ 47 | ${pkgs.gnused}/bin/sed 's/^impacket-//' \ 48 | ${pkgs.coreutils}/bin/sort 49 } 50 case "''${1:-}" in 51 -l|--list) names; exit 0 ;; 52 -h|--help) 53 echo "usage: impacket [<script>] [args…] (no script: pick one)" 54 echo " impacket --list" 55 echo " every script is also a command: impacket-<script>" 56 exit 0 ;; 57 esac 58 if [ "$#" -eq 0 ]; then 59 if [ -t 0 ] && [ -t 1 ]; then 60 sel=$(names | ${pkgs.fzf}/bin/fzf --prompt='impacket › ' \ 61 --preview='${aliases}/bin/impacket-{} --help 2>&1 | head -40' \ 62 --preview-window='right,65%,border-left,wrap') || exit 0 63 [ -n "''${sel:-}" ] || exit 0 64 exec ${aliases}/bin/impacket-"$sel" 65 fi 66 names 67 exit 0 68 fi 69 script="$1"; shift 70 if [ ! -x "${aliases}/bin/impacket-$script" ]; then 71 echo "impacket: no script '$script'" >&2 72 echo "try: impacket --list" >&2 73 exit 2 74 fi 75 exec ${aliases}/bin/impacket-"$script" "$@" 76 ''; 77 78 # So only ONE name is exported from the env: the interpreter. Any offensive script you 79 # download runs with `pentest-python foo.py` and its imports resolve, with 80 # no venv and no collisions. 81 pentest-python = pkgs.runCommand "pentest-python" 82 { 83 meta = { 84 description = "Python with the offensive library set importable"; 85 mainProgram = "pentest-python"; 86 }; 87 } 88 '' 89 mkdir -p $out/bin 90 ln -s ${env}/bin/python3 $out/bin/pentest-python 91 ''; 92 in 93 [ 94 pentest-python 95 impacket-cmd # `impacket` / `impacket --list` 96 impacket # the 70 example scripts, as `secretsdump.py` etc. 97 aliases # ...and as `secretsdump`, collision-guarded 98 ]; 99 100 expectedBins = [ 101 "pentest-python" 102 "secretsdump.py" # impacket's own name 103 "secretsdump" # the suffix-free alias 104 "GetUserSPNs" 105 "impacket" # the discovery command 106 "impacket-split" # held back from the bare name, reachable prefixed 107 ]; 108 109 # Review Focus #1. environment.systemPackages merges every package into ONE 110 # profile with buildEnv, so two packages owning bin/split is a collision, not 111 # a PATH-order question. The probe below is that same merge: it fails to build 112 # if an alias claims a name a real tool already owns, and once it builds we 113 # assert the real tool is what the name resolves to. 114 checkScript = { pkgs, lib }: 115 let 116 probe = pkgs.buildEnv { 117 name = "pentest-python-profile-probe"; 118 paths = [ 119 (import ./_impacket.nix { inherit lib pkgs; }).aliases 120 pkgs.coreutils 121 pkgs.iputils 122 pkgs.samba 123 ]; 124 }; 125 in 126 '' 127 # The bare names must still be the real tools. 128 for n in split ping net smbclient; do 129 t=$(readlink -f ${probe}/bin/$n) 130 case "$t" in 131 *impacket*) 132 echo "pentest-python: '$n' resolves to impacket ($t)" >&2 133 echo " a bare '$n' must stay the real tool; impacket's is impacket-$n" >&2 134 exit 1 ;; 135 esac 136 done 137 # ...and impacket's versions must be reachable under the prefix. 138 for n in split ping net smbclient mimikatz; do 139 if [ ! -e ${probe}/bin/impacket-$n ]; then 140 echo "pentest-python: impacket-$n is missing" >&2 141 exit 1 142 fi 143 done 144 ''; 145 }