diff --git a/README.md b/README.md index dd98c8e..819b24a 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,25 @@ connections during high-concurrency tools such as NetExec; the default is 256. W but host telemetry, PowerShell logging, AMSI, EDR, crash dumps, or pagefile behavior are outside the loader's control. + +### TLS inspection / enterprise decryption + +If an authorized enterprise TLS inspection device presents a different leaf +certificate to the Windows agent than the certificate loaded by the VPS server, +agent certificate pinning will fail. The safest fix is to exempt the PS-Proxy +server domain from TLS decryption so the agent sees the VPS certificate directly. + +For controlled labs where decryption cannot be bypassed, pass the inspected leaf +certificate SHA-256 DER hash explicitly: + +```bash +--agent-cert-pin-override <64-char-sha256-hex-pin> +``` + +Only use this when you control and trust the inspection device. This pins the +agent to the certificate it actually sees, not the certificate file loaded by the +VPS server. + ## Current implementation status Implemented now: diff --git a/cmd/psproxy-server/main.go b/cmd/psproxy-server/main.go index 9ce6c6f..acb86f2 100644 --- a/cmd/psproxy-server/main.go +++ b/cmd/psproxy-server/main.go @@ -34,6 +34,7 @@ func main() { port := flag.Int("port", 443, "TLS listener port") cert := flag.String("cert", "", "TLS fullchain PEM; defaults to /etc/letsencrypt/live//fullchain.pem") key := flag.String("key", "", "TLS private key PEM; defaults to /etc/letsencrypt/live//privkey.pem") + agentCertPinOverride := flag.String("agent-cert-pin-override", "", "override SHA-256 DER certificate pin embedded in staged agents; use only when an authorized TLS inspection device presents a different leaf cert") tun := flag.String("tun", "psproxy0", "TUN interface name for the planned netstack data plane") agentTemplate := flag.String("agent-template", "agent/loader/agent.ps1.tmpl", "PowerShell agent loader template") agentAssemblyFile := flag.String("agent-assembly-b64-file", "", "file containing compressed/base64 PSProxy.Agent.dll; defaults to release/agent_assembly.b64 when present") @@ -74,6 +75,13 @@ func main() { if err != nil { log.Fatalf("certificate pin failed: %v", err) } + if *agentCertPinOverride != "" { + pin, err = normalizeCertPin(*agentCertPinOverride) + if err != nil { + log.Fatalf("invalid --agent-cert-pin-override: %v", err) + } + log.Printf("WARNING: using operator-supplied agent certificate pin override") + } assembly, err := loadAssemblyB64(*agentAssemblyFile) if err != nil { log.Fatalf("agent assembly load failed: %v", err) @@ -708,6 +716,17 @@ type multiFlag []string func (m *multiFlag) String() string { return strings.Join(*m, ",") } func (m *multiFlag) Set(v string) error { *m = append(*m, v); return nil } +func normalizeCertPin(pin string) (string, error) { + normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(pin), ":", ""), " ", "")) + if len(normalized) != 64 { + return "", fmt.Errorf("pin must be 64 hex characters after removing colons/spaces") + } + if _, err := hex.DecodeString(normalized); err != nil { + return "", fmt.Errorf("pin must be hex: %w", err) + } + return normalized, nil +} + func certPin(path string) (string, error) { pemBytes, err := os.ReadFile(path) if err != nil { diff --git a/cmd/psproxy-server/main_test.go b/cmd/psproxy-server/main_test.go index ad26c14..a371172 100644 --- a/cmd/psproxy-server/main_test.go +++ b/cmd/psproxy-server/main_test.go @@ -124,3 +124,23 @@ func TestSingleListenerAcceptReturnsEOFAfterFirstConn(t *testing.T) { t.Fatal("second accept should return EOF") } } + +func TestNormalizeCertPin(t *testing.T) { + pin := "BC:1D:96:47:91:A5:11:B4:95:26:EC:F2:25:35:37:F6:E6:17:0A:1A:19:4F:45:65:9E:88:0C:A7:A4:3D:6C:02" + got, err := normalizeCertPin(pin) + if err != nil { + t.Fatalf("normalize pin failed: %v", err) + } + want := "bc1d964791a511b49526ecf2253537f6e6170a1a194f45659e880ca7a43d6c02" + if got != want { + t.Fatalf("unexpected normalized pin: %s", got) + } +} + +func TestNormalizeCertPinRejectsInvalidPins(t *testing.T) { + for _, pin := range []string{"abc", "zz1d964791a511b49526ecf2253537f6e6170a1a194f45659e880ca7a43d6c02"} { + if _, err := normalizeCertPin(pin); err == nil { + t.Fatalf("expected invalid pin %q to fail", pin) + } + } +}